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 +1 -1
- package/lib/config/rule-collector.js +1 -1
- package/lib/core/res-write/index.js +151 -0
- package/lib/core/res-write/response-rewriter.js +258 -0
- package/lib/core/rulesServer.js +15 -4
- package/lib/core/server-entry/capture-handler.js +4 -186
- package/lib/core/server-entry/response-handler.js +9 -348
- package/lib/core/server-entry/server.js +5 -13
- package/lib/core/shared/buffer.js +23 -0
- package/lib/core/shared/capture.js +203 -0
- package/lib/core/shared/concurrent.js +72 -0
- package/lib/core/shared/initializers.js +93 -0
- package/lib/core/shared/mock-response.js +306 -0
- package/lib/utils/utils.js +1 -1
- package/package.json +1 -1
- package/public/js/app.js +236 -32
- package/public/js/app.js.map +1 -1
- package/rules.txt +10 -1
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
|
-
|
|
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');
|
|
@@ -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
|
+
}
|
package/lib/core/rulesServer.js
CHANGED
|
@@ -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
|
-
#
|
|
24
|
-
#
|
|
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
|
-
* -
|
|
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
|
-
|
|
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,
|