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.
package/index.js CHANGED
@@ -8,7 +8,7 @@ exports.server = require('./lib/core/server-entry/server')
8
8
  // exports.reqRead = require('./lib/reqRead')
9
9
  // exports.reqWrite = require('./lib/reqWrite');
10
10
  // exports.resRead = require('./lib/resRead')
11
- // exports.resWrite = require('./lib/resWrite')
11
+ exports.resWrite = require('./lib/core/res-write')
12
12
  // exports.wsReqRead = require('./lib/wsReqRead');
13
13
  // exports.wsReqWrite = require('./lib/wsReqWrite');
14
14
  // exports.wsResRead = require('./lib/wsResRead');
@@ -15,7 +15,7 @@ class RuleCollector {
15
15
 
16
16
  /**
17
17
  * 添加一条生效的规则
18
- * @param {string} rule - 规则字符串,如 "*.shein.com mockbubu://pathname"
18
+ * @param {string} rule - 规则字符串,如 "* mockbubu://pathname"
19
19
  */
20
20
  addRule(rule) {
21
21
  this.activeRules.add(rule)
@@ -0,0 +1,151 @@
1
+ /**
2
+ * 文件名: index.js
3
+ * 功能: Whistle resWrite 钩子入口
4
+ * 职责:
5
+ * - 接管走了 * whistle.mockbubu:// + * pipe://mockbubu 的请求(响应阶段介入)
6
+ * - 默认只记录请求(透传 + 捕获),请求响应不可改变
7
+ * - 仅当该请求在插件内开启 mock 开关命中时,改写响应体
8
+ *
9
+ * 互斥机制:
10
+ * - 能走 server 钩子的请求,server 钩子打标 req._mockbubuServerHandled = true
11
+ * - resWrite 钩子见此标志直接 req.pipe(res) 放行,不重复处理
12
+ */
13
+
14
+ const {
15
+ getFilename,
16
+ getRule,
17
+ } = require('../../utils/utils')
18
+ const ruleCollector = require('../../config/rule-collector')
19
+ const { PLUGIN_MODE } = require('../../config/const')
20
+ const { awaitShared } = require('../shared/initializers')
21
+ const { captureAndPassthrough, rewriteResponse } = require('./response-rewriter')
22
+
23
+ module.exports = (server, options) => {
24
+ console.log('[mockbubu] 🚀 resWrite 钩子已加载')
25
+ const { storage } = options
26
+
27
+ // 共享初始化(与 server 钩子幂等,已初始化则直接返回缓存)
28
+ let shared = null
29
+ awaitShared(options).then((s) => {
30
+ shared = s
31
+ }).catch((err) => {
32
+ console.error('[mockbubu][resWrite] ❌ 共享初始化失败:', err)
33
+ shared = { _initialized: true, _initError: true }
34
+ })
35
+
36
+ server.on('request', async (req, res) => {
37
+ // 等待初始化完成
38
+ // eslint-disable-next-line no-unmodified-loop-condition
39
+ while (!shared || !shared._initialized) {
40
+ await new Promise((resolve) => setTimeout(resolve, 100))
41
+ }
42
+
43
+ // 初始化失败,安全降级透传
44
+ if (shared._initError) {
45
+ return req.pipe(res)
46
+ }
47
+
48
+ // 【互斥放行】能走 server 钩子的请求,resWrite 直接放行不重复处理
49
+ if (req._mockbubuServerHandled) {
50
+ return req.pipe(res)
51
+ }
52
+
53
+ const { storageAdapter, groupManager, memoryBuffer, pluginModeManager } = shared
54
+
55
+ // resWrite 钩子中 req.headers / req.originalReq.headers 均为上游响应头
56
+ // (见 whistle getOptions isResRules 分支 + setContext oReq.headers = headers)
57
+ // 不能用 shouldInterceptRequest 的请求头 isJsonReq 判断,改用响应头 content-type
58
+ // 注意:JSON 过滤只在"透传+捕获"分支生效——mock 命中时即便上游返回非 JSON
59
+ // 错误响应(如 HTML 错误页、空 body)也必须改写为 mock,不再因非 JSON 提前放行
60
+ const originalReq = req.originalReq
61
+
62
+ // 从 originalReq 提取请求信息(setContext 已注入 url/ruleValue/pattern/method)
63
+ const filename = getFilename(originalReq)
64
+ const rule = getRule(originalReq)
65
+ const { ruleValue, pattern } = originalReq
66
+ const url = filename
67
+ const method = req.method || originalReq.method
68
+ const mode = pluginModeManager.getMode()
69
+
70
+ // 收集生效规则(用于 UI 帮助提示)
71
+ ruleCollector.addRule(rule)
72
+
73
+ try {
74
+ const currentGroupId = await groupManager.getCurrentGroupId()
75
+ const fileConfig = await groupManager.getGroupFileConfig(currentGroupId, filename)
76
+ const { mock, mockVersion } = fileConfig
77
+ const sourceData = await storageAdapter.readFile(`${currentGroupId}/${filename}`)
78
+
79
+ // 透传 + 捕获的公共参数
80
+ const captureParams = {
81
+ req,
82
+ res,
83
+ memoryBuffer,
84
+ currentGroupId,
85
+ filename,
86
+ method,
87
+ url,
88
+ rule,
89
+ pattern,
90
+ ruleValue,
91
+ storage,
92
+ isFirstCapture: true,
93
+ mock,
94
+ }
95
+
96
+ console.log(`[mockbubu] 🚀 resWrite 进入流程 | mode=${mode}, mock=${mock}, 有数据=${!!sourceData} | URL: ${url}`)
97
+
98
+ // 【Mock-Only 模式】对齐 server 钩子语义
99
+ if (mode === PLUGIN_MODE.MOCK_ONLY) {
100
+ // 无 mock / 无数据 → 透传不捕获
101
+ if (!mock || !sourceData) {
102
+ console.log(`[mockbubu][resWrite] Mock-Only 透传(mock=${mock}, 有数据=${!!sourceData})| URL: ${url}`)
103
+ return req.pipe(res)
104
+ }
105
+ // 有 mock + 有数据 → 改写响应体(不捕获)
106
+ console.log(`[mockbubu][resWrite] Mock-Only 改写响应体 | URL: ${url}`)
107
+ return rewriteResponse({
108
+ req,
109
+ res,
110
+ sourceData,
111
+ mockVersion,
112
+ groupManager,
113
+ currentGroupId,
114
+ filename,
115
+ originalReq,
116
+ captureParams,
117
+ })
118
+ }
119
+
120
+ // 【default 模式】
121
+ // mock 开启 + 有源数据 → 改写响应体
122
+ if (mock && sourceData) {
123
+ console.log(`[mockbubu][resWrite] 改写响应体 | URL: ${url}`)
124
+ return rewriteResponse({
125
+ req,
126
+ res,
127
+ sourceData,
128
+ mockVersion,
129
+ groupManager,
130
+ currentGroupId,
131
+ filename,
132
+ originalReq,
133
+ captureParams,
134
+ })
135
+ }
136
+
137
+ // mock 未开启 / 无源数据 → 透传 + 捕获(请求响应不可改变)
138
+ // 仅捕获 JSON 响应;非 JSON(图片/CSS/HTML 等)直接放行不捕获
139
+ const resContentType = (req.headers || {})['content-type'] || ''
140
+ if (!resContentType.includes('application/json')) {
141
+ console.log(`[mockbubu][resWrite] ⏭️ 非 JSON 响应放行 | Content-Type: ${resContentType}`)
142
+ return req.pipe(res)
143
+ }
144
+ console.log(`[mockbubu][resWrite] 透传+捕获(mock=${mock}, 有数据=${!!sourceData})| URL: ${url}`)
145
+ return captureAndPassthrough(captureParams)
146
+ } catch (err) {
147
+ console.error('[mockbubu][resWrite] ❌ 处理失败,降级透传:', err)
148
+ try { req.pipe(res) } catch (e) {}
149
+ }
150
+ })
151
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * 文件名: response-rewriter.js
3
+ * 功能: resWrite 钩子的响应处理逻辑
4
+ * 职责:
5
+ * - captureAndPassthrough: 透传响应 + 旁路捕获源数据(不改写响应)
6
+ * - rewriteResponse: 改写响应体(mock 命中时)
7
+ *
8
+ * resWrite 钩子 req/res 语义(基于 whistle 源码 + 运行时验证):
9
+ * - req 是可读流,流数据 = 上游响应体
10
+ * - req.headers = 上游响应头(含 content-type / content-encoding),与 res.headers 同引用
11
+ * (见 whistle inspectors/res.js: res.headers = req.resHeaders = headers)
12
+ * - req.originalReq = 原始请求对象(拿 url / ruleValue / pattern / body)
13
+ * - req.getSession(cb) = 拿完整 session(响应结束后回调)
14
+ * - res = 写回客户端的 encoder Transform 流(objectMode)
15
+ *
16
+ * 透传:req.pipe(res)(响应头由 whistle 外层处理)
17
+ * 改写:按上游 content-encoding 压缩 mock body 后 res.end(body) 写回客户端
18
+ * (resWrite 钩子改不了响应头,必须使头体匹配,见 sendMockBody / compressBody)。
19
+ */
20
+
21
+ const zlib = require('zlib')
22
+ const {
23
+ handleBuffer2String,
24
+ withTryCatch,
25
+ } = require('../../utils/utils')
26
+ const { matchByConditions } = require('../shared/mock-response')
27
+ const { saveSourceData } = require('../shared/capture')
28
+ const { MATCH_MODE_VALUE } = require('../../config/const')
29
+
30
+ /**
31
+ * 透传响应 + 旁路捕获源数据(不改写响应)
32
+ *
33
+ * 适用场景:
34
+ * - mock 未开启
35
+ * - 无源数据
36
+ * - 条件匹配模式全不命中
37
+ *
38
+ * @param {Object} params
39
+ */
40
+ function captureAndPassthrough(params) {
41
+ const {
42
+ req, res,
43
+ memoryBuffer, currentGroupId, filename,
44
+ method, url, rule, pattern, ruleValue,
45
+ storage, isFirstCapture, mock,
46
+ } = params
47
+
48
+ const headers = req.headers || {}
49
+ const contentType = headers['content-type'] || ''
50
+ const encoding = headers['content-encoding']
51
+ const isJson = contentType.includes('application/json')
52
+
53
+ // 旁路 buffer 响应体(pipe 同时透传,on('data') 不影响 pipe)
54
+ const chunks = []
55
+ if (isJson) {
56
+ req.on('data', (c) => chunks.push(c))
57
+ }
58
+
59
+ // 透传响应给客户端(响应头由 whistle 外层处理)
60
+ req.pipe(res)
61
+
62
+ // 响应结束后旁路捕获
63
+ req.on('end', withTryCatch(async () => {
64
+ if (!isJson) return
65
+
66
+ const body = chunks.length ? Buffer.concat(chunks) : null
67
+ if (!body) {
68
+ console.log(`[mockbubu][resWrite] ⚠️ 响应体为空,跳过捕获: ${filename}`)
69
+ return
70
+ }
71
+
72
+ let content
73
+ try {
74
+ content = await handleBuffer2String({ body, encoding })
75
+ } catch (err) {
76
+ console.error(`[mockbubu][resWrite] ❌ 解压失败: ${filename}`, err.message)
77
+ return
78
+ }
79
+
80
+ req.getSession(async (session) => {
81
+ if (!session) {
82
+ console.log(`[mockbubu][resWrite] ⚠️ session 为空,跳过: ${filename}`)
83
+ return
84
+ }
85
+ try {
86
+ await saveSourceData({
87
+ session,
88
+ content,
89
+ memoryBuffer,
90
+ currentGroupId,
91
+ filename,
92
+ isFirstCapture,
93
+ mock,
94
+ method,
95
+ rule,
96
+ pattern,
97
+ ruleValue,
98
+ url,
99
+ storage,
100
+ })
101
+ console.log(`[mockbubu][resWrite] ✅ 捕获完成: ${filename}`)
102
+ } catch (err) {
103
+ console.error(`[mockbubu][resWrite] ❌ 保存失败: ${filename}`, err.message)
104
+ }
105
+ })
106
+ }))
107
+
108
+ req.on('error', (err) => {
109
+ console.error(`[mockbubu][resWrite] ❌ 响应流错误: ${filename}`, err.message)
110
+ try { res.end() } catch (e) {}
111
+ })
112
+ }
113
+
114
+ /**
115
+ * 消费上游响应体(改写场景,不需要上游 body,但要 drain 防止 backpressure)
116
+ *
117
+ * 上游响应体可能很大,若不消费会涨满 decoder readable buffer,backpressure 传回
118
+ * 隧道 socket,影响 encoder 写回 mock body。drain 到 end 即可释放。
119
+ *
120
+ * @param {Object} req - 上游响应体可读流(decoder)
121
+ * @returns {Promise<void>} 上游响应体结束
122
+ */
123
+ function drainReq(req) {
124
+ return new Promise((resolve) => {
125
+ let resolved = false
126
+ const done = () => {
127
+ if (resolved) return
128
+ resolved = true
129
+ resolve()
130
+ }
131
+ req.on('data', () => {})
132
+ req.on('end', done)
133
+ req.on('error', (err) => {
134
+ console.error(`[mockbubu][resWrite] ❌ drain 错误: ${err.message}`)
135
+ done()
136
+ })
137
+ req.on('close', done)
138
+ })
139
+ }
140
+
141
+ /**
142
+ * 按上游 content-encoding 压缩 mock body
143
+ *
144
+ * resWrite 钩子改不了响应头(CONNECT 隧道只传 body),客户端收到的 content-encoding
145
+ * 仍是上游的(如 br/gzip)。若写回明文 mock body,客户端按压缩格式解压会失败 → 0 bytes。
146
+ * 因此把明文 mock body 按上游编码重新压缩,使头体匹配,客户端正常解压。
147
+ *
148
+ * @param {Buffer} body - 明文 mock body
149
+ * @param {string} encoding - 上游 content-encoding(br/gzip/deflate)
150
+ * @returns {Buffer} 压缩后的 body(失败或无编码时返回原 body)
151
+ */
152
+ function compressBody(body, encoding) {
153
+ if (!encoding) return body
154
+ try {
155
+ if (encoding === 'br') return zlib.brotliCompressSync(body)
156
+ if (encoding === 'gzip') return zlib.gzipSync(body)
157
+ if (encoding === 'deflate') return zlib.deflateSync(body)
158
+ } catch (err) {
159
+ console.error(`[mockbubu][resWrite] ❌ 压缩失败(encoding=${encoding}),降级明文: ${err.message}`)
160
+ }
161
+ return body
162
+ }
163
+
164
+ /**
165
+ * 写回 mock body 给客户端
166
+ *
167
+ * res 是 whistle transproto 的 encoder Transform 流(objectMode):
168
+ * res.end(body) → _transform → pack(body) + pack() 终止帧 → pipe 到 socket。
169
+ *
170
+ * 响应头改不了(CONNECT 隧道只传 body),上游 content-encoding 会被原样发给客户端,
171
+ * 故 mock body 必须按上游编码压缩使头体匹配(见 compressBody)。
172
+ *
173
+ * @param {Object} req - 上游响应流(decoder),req.headers 含上游 content-encoding
174
+ * @param {Object} res - encoder Transform 流
175
+ * @param {string|Buffer} body - 明文 mock 响应体
176
+ * @param {string} filename - 文件名(日志用)
177
+ * @param {string} label - 版本/匹配标签(日志用)
178
+ */
179
+ function sendMockBody(req, res, body, filename, label) {
180
+ try {
181
+ const bodyBuf = Buffer.isBuffer(body) ? body : Buffer.from(String(body ?? ''))
182
+ const encoding = (req.headers || {})['content-encoding']
183
+ const outBuf = compressBody(bodyBuf, encoding)
184
+ res.end(outBuf)
185
+ console.log(`[mockbubu][resWrite] ✅ mock body 已写入: ${filename} (label=${label}, ${bodyBuf.length} bytes${encoding ? ` → ${encoding}=${outBuf.length}` : ''})`)
186
+ } catch (err) {
187
+ console.error(`[mockbubu][resWrite] ❌ 写回 mock body 失败: ${filename}`, err.message, err.stack)
188
+ try { res.end() } catch (e) {}
189
+ }
190
+ }
191
+
192
+ /**
193
+ * 改写响应体(mock 命中时)
194
+ *
195
+ * 流程:
196
+ * 1. 条件匹配模式:先判断命中(不消费 req 流)
197
+ * - 命中 → drain 上游 + 写回 mock body
198
+ * - 全不命中 → 退回 captureAndPassthrough
199
+ * 2. 版本直选 / 源数据 → drain 上游 + 写回 mock body
200
+ *
201
+ * @param {Object} params
202
+ */
203
+ async function rewriteResponse(params) {
204
+ const {
205
+ req, res,
206
+ sourceData, mockVersion,
207
+ groupManager, currentGroupId, filename, originalReq,
208
+ captureParams,
209
+ } = params
210
+
211
+ console.log(`[mockbubu][resWrite] 🔧 rewriteResponse 进入 | mockVersion=${mockVersion || 'source'} | ${filename}`)
212
+
213
+ // 【条件匹配模式】先判断命中(不消费上游响应流)
214
+ if (mockVersion === MATCH_MODE_VALUE) {
215
+ const matchResult = await matchByConditions({
216
+ groupManager,
217
+ currentGroupId,
218
+ filename,
219
+ originalReq,
220
+ })
221
+
222
+ // 全不命中 → 退回透传 + 捕获
223
+ if (!matchResult.matched) {
224
+ console.log(`[mockbubu][resWrite] 条件匹配全不命中,退回透传+捕获: ${filename}`)
225
+ return captureAndPassthrough(captureParams)
226
+ }
227
+
228
+ // 命中 → drain 上游 + 写回 mock body
229
+ console.log(`[mockbubu][resWrite] 🔧 条件命中,准备 drain | ${filename}`)
230
+ await drainReq(req)
231
+ const body = JSON.stringify(matchResult.content ?? {})
232
+ sendMockBody(req, res, body, filename, `match:${matchResult.ruleName}`)
233
+ return
234
+ }
235
+
236
+ // 【版本直选 / 源数据】drain 上游 + 写回 mock body
237
+ console.log(`[mockbubu][resWrite] 🔧 版本/源数据,准备 drain | ${filename}`)
238
+ await drainReq(req)
239
+
240
+ let body = JSON.parse(sourceData)?.res?.body
241
+
242
+ // 版本直选:优先用版本数据
243
+ if (mockVersion && groupManager && currentGroupId && filename) {
244
+ const versionData = await groupManager.getGroupVersionContent(currentGroupId, filename, mockVersion)
245
+ if (versionData) {
246
+ body = JSON.stringify(versionData)
247
+ }
248
+ // 版本不存在时降级到源数据
249
+ }
250
+
251
+ sendMockBody(req, res, body, filename, mockVersion || 'source')
252
+ }
253
+
254
+ module.exports = {
255
+ captureAndPassthrough,
256
+ rewriteResponse,
257
+ drainReq,
258
+ }
@@ -1,6 +1,14 @@
1
1
  /**
2
2
  * rulesServer 钩子
3
3
  * 动态返回 mockbubu 规则,根据启用状态控制
4
+ *
5
+ * 三条规则分工:
6
+ * 1) * mockbubu:// → server 钩子(能走 server 钩子的请求,原完整逻辑)
7
+ * 2) * whistle.mockbubu:// → plugin 上下文(让 resWrite 能拿请求详情)
8
+ * 3) * pipe://mockbubu → resWrite 钩子(走了 whistle.mockbubu:// 的请求,响应阶段介入)
9
+ *
10
+ * 互斥保证:server 钩子处理请求时打标 req._mockbubuServerHandled,
11
+ * resWrite 钩子见此标志直接 passThrough,不重复处理。
4
12
  */
5
13
  module.exports = (server, options) => {
6
14
  server.on('request', (req, res) => {
@@ -17,12 +25,15 @@ module.exports = (server, options) => {
17
25
  }
18
26
 
19
27
  // 启用时返回 mockbubu 规则
20
- // 这里返回通配规则,让所有请求都经过 server 钩子判断
21
28
  const rules = `
22
29
  # mockbubu 插件规则
23
- # 所有请求都会经过 mockbubu server 钩子处理
24
- # 具体是否 mock server 钩子中的 mock 开关决定
25
- * mockbubu://
30
+ # 1) * mockbubu:// → server 钩子(未命中 host 转发的请求,原捕获 + mock 改写)
31
+ # 2) * whistle.mockbubu:// plugin 上下文(让 resWrite 能拿请求详情)
32
+ # 3) * pipe://mockbubu → resWrite 钩子(走了 whistle.mockbubu:// 的请求,响应阶段介入)
33
+ # 互斥:server 钩子已处理的请求,resWrite 直接 passThrough
34
+ # * mockbubu://
35
+ # * whistle.mockbubu://
36
+ # * pipe://mockbubu
26
37
  `.trim()
27
38
 
28
39
  console.log('[mockbubu] ✅ 插件已启用,返回规则')
@@ -1,195 +1,13 @@
1
1
  /**
2
2
  * 文件名: capture-handler.js
3
- * 功能: 源数据捕获逻辑
3
+ * 功能: 源数据捕获逻辑(薄封装,实现已抽取到 shared)
4
4
  * 职责:
5
- * - 捕获真实 API 响应数据
6
- * - 处理响应体解压缩
7
- * - 保存源数据到存储 (file.json)
8
- * - 更新文件配置
9
- * - 并发控制
10
- */
11
-
12
- const {
13
- setApiListUpdated,
14
- handleBuffer2String,
15
- withTryCatch,
16
- } = require('../../utils/utils')
17
-
18
- /**
19
- * 捕获真实 API 响应数据
20
- *
21
- * @param {Object} params - 参数对象
22
- * @param {Object} params.req - Whistle 请求对象
23
- * @param {Object} params.res - Whistle 响应对象
24
- * @param {Object} params.storageAdapter - 存储适配器
25
- * @param {Object} params.groupManager - 组管理器
26
- * @param {Object} params.memoryBuffer - 内存缓冲区
27
- * @param {string} params.currentGroupId - 当前组ID
28
- * @param {string} params.filename - 文件名
29
- * @param {boolean} params.isFirstCapture - 是否首次捕获
30
- * @param {boolean} params.mock - Mock 状态
31
- * @param {Object} params.requestInfo - 请求信息 { method, rule, pattern, ruleValue, url }
32
- * @param {Map} params.capturingFiles - 捕获中的文件 Map
33
- * @param {Object} params.storage - Whistle storage 对象
34
- */
35
- async function captureResponse(params) {
36
- const {
37
- req,
38
- res,
39
- storageAdapter,
40
- groupManager,
41
- memoryBuffer,
42
- currentGroupId,
43
- filename,
44
- isFirstCapture,
45
- mock,
46
- requestInfo,
47
- capturingFiles,
48
- storage,
49
- } = params
50
-
51
- const { method, rule, pattern, ruleValue, url } = requestInfo
52
- const captureKey = `${currentGroupId}/${filename}`
53
-
54
- console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 🚀 captureResponse 函数开始执行: ${filename}`)
55
-
56
- // 设置捕获标志
57
- let resolveCapture
58
- const capturePromise = new Promise(resolve => { resolveCapture = resolve })
59
- capturingFiles.set(captureKey, capturePromise)
60
-
61
- console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 🌐 准备调用 req.request(): ${filename}`)
62
-
63
- // 发起真实请求
64
- const client = req.request((svrRes) => {
65
- console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 🔵 req.request 回调已执行: ${filename}`)
66
- const encoding = svrRes.headers['content-encoding']
67
- let body
68
-
69
- svrRes.on('data', (data) => {
70
- console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 📥 接收数据块: ${filename}, 大小: ${data?.length || 0}`)
71
- body = body ? Buffer.concat([body, data]) : data
72
- })
73
-
74
- svrRes.on('end', withTryCatch(async () => {
75
- try {
76
- if (!body) {
77
- console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ⚠️ 响应体为空,跳过捕获: ${filename}`)
78
- return
79
- }
80
-
81
- const resContentType = svrRes.headers['content-type'] || ''
82
- if (!resContentType.includes('application/json')) {
83
- console.log(`[mockbubu] ⏭️ 非 JSON 响应,跳过捕获: ${filename} (Content-Type: ${resContentType})`)
84
- return
85
- }
86
-
87
- console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 📦 响应接收完成,开始解压: ${filename}`)
88
- // 解压响应体
89
- const content = await handleBuffer2String({ body, encoding })
90
- console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ✅ 解压完成,调用 req.getSession(): ${filename}`)
91
-
92
- // 获取完整的请求响应数据 (用于构建源数据)
93
- req.getSession(async (session) => {
94
- console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 🔍 getSession 回调已触发: ${filename}`)
95
- console.log('[mockbubu DEBUG] session.req.body存在:', !!session?.req?.body)
96
- console.log('[mockbubu DEBUG] session.req.body类型:', typeof session?.req?.body)
97
- if (session?.req?.body) {
98
- console.log('[mockbubu DEBUG] session.req.body前200字符:', String(session.req.body).substring(0, 200))
99
- }
100
- try {
101
- // 如果设置了 enable://hide 会获取到空数据
102
- if (!session) {
103
- console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ⚠️ session 为空(可能设置了 enable://hide): ${filename}`)
104
- return
105
- }
106
-
107
- console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 💾 开始保存源数据: ${filename}`)
108
- // 保存源数据和文件配置
109
- await saveSourceData({
110
- session,
111
- content,
112
- storageAdapter,
113
- groupManager,
114
- memoryBuffer,
115
- currentGroupId,
116
- filename,
117
- isFirstCapture,
118
- mock,
119
- method,
120
- rule,
121
- pattern,
122
- ruleValue,
123
- url,
124
- storage,
125
- })
126
-
127
- console.log(`[mockbubu] ✅ 捕获完成: ${filename}`)
128
- } finally {
129
- // 捕获完成,清除标志并通知等待的请求
130
- resolveCapture()
131
- capturingFiles.delete(captureKey)
132
- }
133
- })
134
- } catch (err) {
135
- console.error(`[mockbubu] ❌ 捕获过程出错: ${filename}`, err)
136
- // 出错也要清除标志
137
- resolveCapture()
138
- capturingFiles.delete(captureKey)
139
- }
140
- }))
141
-
142
- // 将响应透传给客户端
143
- svrRes.pipe(res)
144
- })
145
-
146
- console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ✅ req.request() 已调用,准备 pipe 数据: ${filename}`)
147
-
148
- req.pipe(client)
149
-
150
- console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ✅ req.pipe(client) 已完成: ${filename}`)
151
- }
152
-
153
- /**
154
- * 保存源数据到内存缓冲区
5
+ * - captureResponse / saveSourceData 重新导出自 shared
155
6
  *
156
- * 职责:
157
- * - 不写入文件系统(文件仅在用户点击"保存"时创建)
158
- * - 将完整会话数据添加到内存缓冲区(供前端轮询拉取)
159
- * - 刷新页面后,memoryBuffer 自动清空,未保存的数据消失
160
- *
161
- * @param {Object} params - 参数对象
7
+ * 实现位置: lib/core/shared/capture.js
162
8
  */
163
- async function saveSourceData(params) {
164
- const {
165
- session,
166
- content,
167
- memoryBuffer,
168
- currentGroupId,
169
- url,
170
- method,
171
- storage,
172
- } = params
173
-
174
- // 1. 构建完整会话数据
175
- const sourceDataToSave = JSON.parse(JSON.stringify(session))
176
- sourceDataToSave.res.body = content
177
9
 
178
- // 2. 添加到内存缓冲区(供前端轮询拉取)
179
- // MemoryBuffer 内部会检查暂存区是否已存在,已存在则跳过
180
- memoryBuffer.add(currentGroupId, url, {
181
- method,
182
- status: session.res.statusCode,
183
- session: sourceDataToSave,
184
- })
185
-
186
- // 3. ⚠️ 不要在捕获时创建文件配置
187
- // 只有用户明确点击"保存"按钮后,才会创建 file.json + versions/
188
- // 捕获的数据仅保存在 memoryBuffer 中,刷新后自动清空
189
-
190
- // 4. 标记 API 列表已更新
191
- await setApiListUpdated(storage, true)
192
- }
10
+ const { captureResponse, saveSourceData } = require('../shared/capture')
193
11
 
194
12
  module.exports = {
195
13
  captureResponse,