whistle.mockbubu 2.0.0 → 2.1.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.
Files changed (56) hide show
  1. package/README.md +38 -0
  2. package/index.js +3 -3
  3. package/lib/config/const.js +138 -0
  4. package/lib/config/rule-collector.js +81 -0
  5. package/lib/constants.js +62 -0
  6. package/lib/core/memory-buffer/index.js +207 -0
  7. package/lib/core/memory-buffer/shared-instance.js +15 -0
  8. package/lib/core/plugin-mode-manager.js +74 -0
  9. package/lib/core/resRulesServer.js +14 -0
  10. package/lib/core/rulesServer.js +31 -0
  11. package/lib/core/server-entry/capture-handler.js +191 -0
  12. package/lib/core/server-entry/request-interceptor.js +82 -0
  13. package/lib/core/server-entry/response-handler.js +147 -0
  14. package/lib/core/server-entry/server.js +230 -0
  15. package/lib/storage/group-manager.js +627 -0
  16. package/lib/storage/storage-adapter.js +712 -0
  17. package/lib/storage/storage-v3.js +1418 -0
  18. package/lib/uiServer/index.js +61 -24
  19. package/lib/uiServer/router/export/import-export-router.js +459 -0
  20. package/lib/uiServer/router/files/api-list-router.js +150 -0
  21. package/lib/uiServer/router/files/batch-operations-router.js +185 -0
  22. package/lib/uiServer/router/files/file-config-router.js +118 -0
  23. package/lib/uiServer/router/files/file-crud-router.js +212 -0
  24. package/lib/uiServer/router/files/file-save-router.js +146 -0
  25. package/lib/uiServer/router/files/version-router.js +260 -0
  26. package/lib/uiServer/router/global/plugin-control.js +135 -0
  27. package/lib/uiServer/router/global/system-info-router.js +386 -0
  28. package/lib/uiServer/router/{group-router.js → groups/group-router.js} +21 -20
  29. package/lib/uiServer/router/index.js +38 -1521
  30. package/lib/uiServer/utils/router-helpers.js +100 -0
  31. package/lib/uiServer/utils/util.js +172 -0
  32. package/lib/uiServer/{validator.js → utils/validator.js} +11 -6
  33. package/lib/utils/archive-utils.js +788 -0
  34. package/lib/utils/error-handler.js +173 -0
  35. package/lib/utils/logger.js +79 -0
  36. package/lib/utils/path-utils.js +147 -0
  37. package/lib/utils/performance.js +265 -0
  38. package/lib/utils/utils.js +541 -0
  39. package/package.json +2 -2
  40. package/public/js/app.js +3732 -1929
  41. package/public/js/app.js.map +1 -1
  42. package/public/js/chunk-vendors.js +5098 -3965
  43. package/public/js/chunk-vendors.js.map +1 -1
  44. package/rules.txt +1 -1
  45. package/CHANGELOG_GROUP_FEATURE.md +0 -468
  46. package/CHANGELOG_P0_FIXES.md +0 -412
  47. package/CHANGELOG_P1_OPTIMIZATIONS.md +0 -292
  48. package/CLAUDE.md +0 -436
  49. package/GROUP_FEATURE_DESIGN.md +0 -520
  50. package/lib/const.js +0 -47
  51. package/lib/group-manager.js +0 -491
  52. package/lib/resRulesServer.js +0 -9
  53. package/lib/server.js +0 -249
  54. package/lib/uiServer/router/version-router.js +0 -205
  55. package/lib/uiServer/util.js +0 -153
  56. package/lib/utils.js +0 -409
@@ -0,0 +1,191 @@
1
+ /**
2
+ * 文件名: capture-handler.js
3
+ * 功能: 源数据捕获逻辑
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
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 📦 响应接收完成,开始解压: ${filename}`)
82
+ // 解压响应体
83
+ const content = await handleBuffer2String({ body, encoding })
84
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ✅ 解压完成,调用 req.getSession(): ${filename}`)
85
+
86
+ // 获取完整的请求响应数据 (用于构建源数据)
87
+ req.getSession(async (session) => {
88
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 🔍 getSession 回调已触发: ${filename}`)
89
+ console.log('[mockbubu DEBUG] session.req.body存在:', !!session?.req?.body)
90
+ console.log('[mockbubu DEBUG] session.req.body类型:', typeof session?.req?.body)
91
+ if (session?.req?.body) {
92
+ console.log('[mockbubu DEBUG] session.req.body前200字符:', String(session.req.body).substring(0, 200))
93
+ }
94
+ try {
95
+ // 如果设置了 enable://hide 会获取到空数据
96
+ if (!session) {
97
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ⚠️ session 为空(可能设置了 enable://hide): ${filename}`)
98
+ return
99
+ }
100
+
101
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 💾 开始保存源数据: ${filename}`)
102
+ // 保存源数据和文件配置
103
+ await saveSourceData({
104
+ session,
105
+ content,
106
+ storageAdapter,
107
+ groupManager,
108
+ memoryBuffer,
109
+ currentGroupId,
110
+ filename,
111
+ isFirstCapture,
112
+ mock,
113
+ method,
114
+ rule,
115
+ pattern,
116
+ ruleValue,
117
+ url,
118
+ storage,
119
+ })
120
+
121
+ console.log(`[mockbubu] ✅ 捕获完成: ${filename}`)
122
+ } finally {
123
+ // 捕获完成,清除标志并通知等待的请求
124
+ resolveCapture()
125
+ capturingFiles.delete(captureKey)
126
+ }
127
+ })
128
+ } catch (err) {
129
+ console.error(`[mockbubu] ❌ 捕获过程出错: ${filename}`, err)
130
+ // 出错也要清除标志
131
+ resolveCapture()
132
+ capturingFiles.delete(captureKey)
133
+ }
134
+ }))
135
+
136
+ // 将响应透传给客户端
137
+ svrRes.pipe(res)
138
+ })
139
+
140
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ✅ req.request() 已调用,准备 pipe 数据: ${filename}`)
141
+
142
+ req.pipe(client)
143
+
144
+ console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] ✅ req.pipe(client) 已完成: ${filename}`)
145
+ }
146
+
147
+ /**
148
+ * 保存源数据到内存缓冲区
149
+ *
150
+ * 职责:
151
+ * - 不写入文件系统(文件仅在用户点击"保存"时创建)
152
+ * - 将完整会话数据添加到内存缓冲区(供前端轮询拉取)
153
+ * - 刷新页面后,memoryBuffer 自动清空,未保存的数据消失
154
+ *
155
+ * @param {Object} params - 参数对象
156
+ */
157
+ async function saveSourceData(params) {
158
+ const {
159
+ session,
160
+ content,
161
+ memoryBuffer,
162
+ currentGroupId,
163
+ url,
164
+ method,
165
+ storage,
166
+ } = params
167
+
168
+ // 1. 构建完整会话数据
169
+ const sourceDataToSave = JSON.parse(JSON.stringify(session))
170
+ sourceDataToSave.res.body = content
171
+
172
+ // 2. 添加到内存缓冲区(供前端轮询拉取)
173
+ // MemoryBuffer 内部会检查暂存区是否已存在,已存在则跳过
174
+ memoryBuffer.add(currentGroupId, url, {
175
+ method,
176
+ status: session.res.statusCode,
177
+ session: sourceDataToSave,
178
+ })
179
+
180
+ // 3. ⚠️ 不要在捕获时创建文件配置
181
+ // 只有用户明确点击"保存"按钮后,才会创建 file.json + versions/
182
+ // 捕获的数据仅保存在 memoryBuffer 中,刷新后自动清空
183
+
184
+ // 4. 标记 API 列表已更新
185
+ await setApiListUpdated(storage, true)
186
+ }
187
+
188
+ module.exports = {
189
+ captureResponse,
190
+ saveSourceData,
191
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 文件名: request-interceptor.js
3
+ * 功能: 请求拦截判断逻辑
4
+ * 职责:
5
+ * - 判断请求是否需要拦截
6
+ * - 判断是否为 JSON 请求
7
+ * - 检查插件工作模式
8
+ * - 收集生效规则
9
+ */
10
+
11
+ const {
12
+ getFilename,
13
+ getRule,
14
+ isJsonReq,
15
+ } = require('../../utils/utils')
16
+ const ruleCollector = require('../../config/rule-collector')
17
+ const { PLUGIN_MODE, DEFAULT_PLUGIN_MODE } = require('../../config/const')
18
+
19
+ /**
20
+ * 检查插件是否启用(兼容函数)
21
+ * @param {Object} storage - Whistle storage 对象
22
+ * @returns {boolean}
23
+ * @deprecated 使用 getPluginMode 替代
24
+ */
25
+ function isPluginEnabled(storage) {
26
+ // 任何模式都算"启用"
27
+ return true
28
+ }
29
+
30
+ /**
31
+ * 判断是否应该拦截该请求
32
+ * @param {Object} req - Whistle 请求对象
33
+ * @param {Object} storage - Whistle storage 对象
34
+ * @param {string} cachedMode - 缓存的插件模式 ('capture' | 'mock-only')
35
+ * @returns {Object} { shouldIntercept, filename, rule, reason, mode }
36
+ */
37
+ function shouldInterceptRequest(req, storage, cachedMode = DEFAULT_PLUGIN_MODE) {
38
+ const { originalReq, method } = req
39
+ const { headers, ruleValue, pattern } = originalReq
40
+
41
+ // 1. 使用缓存的插件工作模式(避免异步读取)
42
+ const mode = cachedMode
43
+
44
+ // 2. 提取文件名和规则
45
+ const filename = getFilename(originalReq)
46
+ const rule = getRule(originalReq)
47
+
48
+ // 收集生效的规则(用于 UI 帮助提示)
49
+ ruleCollector.addRule(rule)
50
+
51
+ // 3. 判断是否为 JSON 请求
52
+ if (!isJsonReq(headers)) {
53
+ return {
54
+ shouldIntercept: false,
55
+ reason: 'not-json',
56
+ message: '非 JSON 请求',
57
+ filename,
58
+ rule,
59
+ headers,
60
+ mode,
61
+ }
62
+ }
63
+
64
+ // 4. 应该拦截
65
+ return {
66
+ shouldIntercept: true,
67
+ filename,
68
+ rule,
69
+ method,
70
+ url: filename, // 使用处理后的 filename 作为 url,确保与 filename 一致
71
+ pattern,
72
+ ruleValue,
73
+ headers,
74
+ mode, // 携带当前模式,供后续流程使用
75
+ }
76
+ }
77
+
78
+ module.exports = {
79
+ isPluginEnabled,
80
+ shouldInterceptRequest,
81
+ PLUGIN_MODE, // 导出模式常量
82
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * 文件名: response-handler.js
3
+ * 功能: Mock 响应返回逻辑
4
+ * 职责:
5
+ * - 返回 Mock 数据 (源数据或版本数据)
6
+ * - 处理版本切换
7
+ * - 处理响应头
8
+ */
9
+
10
+ /**
11
+ * 返回 Mock 响应数据
12
+ * @param {Object} res - Whistle 响应对象
13
+ * @param {string} sourceData - 源数据 (file.json 的字符串内容)
14
+ * @param {Object} options - 配置选项
15
+ * @param {string} options.mockVersion - Mock 版本名称 (null 表示使用源数据)
16
+ * @param {Object} options.groupManager - 组管理器实例
17
+ * @param {string} options.currentGroupId - 当前组ID
18
+ * @param {string} options.filename - 文件名
19
+ */
20
+ async function sendMockResponse(res, sourceData, options = {}) {
21
+ const { mockVersion, groupManager, currentGroupId, filename } = options
22
+
23
+ // 解析源数据 (storageAdapter.readFile 返回的是 session 对象的 JSON 字符串)
24
+ const session = JSON.parse(sourceData)
25
+ const sourceResponse = session?.res
26
+ const { statusCode, statusMessage, headers } = sourceResponse
27
+
28
+ // 设置响应头标记 (来自 Mock)
29
+ headers['from-mockbubu-source'] = 'true'
30
+ headers['x-mockbubu-mocked'] = 'true'
31
+ delete headers['content-encoding']
32
+ delete headers['content-length']
33
+
34
+ res.writeHead(statusCode, statusMessage, headers)
35
+
36
+ // 检查是否需要使用版本数据 (用户编辑的 Mock 数据)
37
+ if (mockVersion && groupManager && currentGroupId && filename) {
38
+ const versionData = await groupManager.getGroupVersionContent(currentGroupId, filename, mockVersion)
39
+ if (versionData) {
40
+ // 返回版本数据
41
+ res.end(JSON.stringify(versionData))
42
+ return
43
+ }
44
+ // 版本不存在时降级到源数据
45
+ }
46
+
47
+ // 返回源数据 (原始捕获的响应体)
48
+ res.end(sourceResponse.body)
49
+ }
50
+
51
+ /**
52
+ * 检查是否需要捕获源数据
53
+ * 如果文件已存在配置,则透传;如果是首次访问,则需要捕获
54
+ *
55
+ * @param {Object} params
56
+ * @param {Object} params.storageAdapter - 存储适配器
57
+ * @param {Object} params.groupManager - 组管理器
58
+ * @param {string} params.currentGroupId - 当前组ID
59
+ * @param {string} params.filename - 文件名
60
+ * @returns {Promise<{shouldCapture: boolean, isFirstCapture: boolean}>}
61
+ */
62
+ async function checkShouldCapture(params) {
63
+ const { storageAdapter, groupManager, currentGroupId, filename } = params
64
+
65
+ // 检查源数据文件是否已存在
66
+ const configKey = groupManager.getGroupConfigKey(currentGroupId, filename)
67
+ const hasConfig = await storageAdapter.hasFileInIndex(configKey)
68
+
69
+ if (hasConfig) {
70
+ // 源数据已存在,不需要重复捕获
71
+ console.log(`[mockbubu] ⏭️ 源数据已存在,跳过捕获并透传: ${filename}`)
72
+ return {
73
+ shouldCapture: false,
74
+ isFirstCapture: false,
75
+ }
76
+ } else {
77
+ // 源数据不存在 - 首次捕获
78
+ console.log(`[mockbubu] 🎣 首次捕获源数据: ${filename}`)
79
+ return {
80
+ shouldCapture: true,
81
+ isFirstCapture: true,
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 处理并发捕获时的等待逻辑
88
+ * @param {Map} capturingFiles - 捕获中的文件 Map
89
+ * @param {string} captureKey - 捕获 key
90
+ * @param {Object} params - 参数对象
91
+ * @returns {Promise<Object>} { handled, response }
92
+ */
93
+ async function handleConcurrentCapture(capturingFiles, captureKey, params) {
94
+ const {
95
+ res,
96
+ storageAdapter,
97
+ groupManager,
98
+ currentGroupId,
99
+ filename,
100
+ mockVersion,
101
+ filePath,
102
+ } = params
103
+
104
+ if (!capturingFiles.has(captureKey)) {
105
+ return { handled: false }
106
+ }
107
+
108
+ // 正在捕获源数据中,等待捕获完成
109
+ console.log(`[mockbubu] ⏳ 检测到并发请求,等待源数据捕获完成: ${filename}`)
110
+
111
+ try {
112
+ // 等待第一个请求捕获完成(最多等待10秒)
113
+ await Promise.race([
114
+ capturingFiles.get(captureKey),
115
+ new Promise((_resolve, reject) => setTimeout(() => reject(new Error('捕获超时')), 10000)),
116
+ ])
117
+
118
+ // 捕获完成,重新读取源数据
119
+ const freshSourceData = await storageAdapter.readFile(filePath)
120
+ if (freshSourceData) {
121
+ console.log(`[mockbubu] ✅ 等待成功,返回捕获的源数据: ${filename}`)
122
+
123
+ await sendMockResponse(res, freshSourceData, {
124
+ mockVersion,
125
+ groupManager,
126
+ currentGroupId,
127
+ filename,
128
+ })
129
+
130
+ return { handled: true, response: 'success' }
131
+ } else {
132
+ // 等待后仍无源数据,降级到透传
133
+ console.warn(`[mockbubu] ⚠️ 等待后仍无源数据,降级透传: ${filename}`)
134
+ return { handled: true, response: 'passthrough' }
135
+ }
136
+ } catch (err) {
137
+ // 等待超时或失败,降级到透传
138
+ console.error(`[mockbubu] ❌ 等待捕获失败,降级透传: ${filename}`, err.message)
139
+ return { handled: true, response: 'passthrough' }
140
+ }
141
+ }
142
+
143
+ module.exports = {
144
+ sendMockResponse,
145
+ checkShouldCapture,
146
+ handleConcurrentCapture,
147
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * 文件名: server.js
3
+ * 功能: Whistle 插件主入口(请求拦截钩子)
4
+ * 职责:
5
+ * - 初始化存储和组管理器
6
+ * - 协调请求拦截、响应返回、数据捕获等模块
7
+ * - 处理请求生命周期
8
+ */
9
+
10
+ const GroupManager = require('../../storage/group-manager')
11
+ const StorageAdapter = require('../../storage/storage-adapter')
12
+ const memoryBuffer = require('../memory-buffer/shared-instance')
13
+ const {
14
+ shouldInterceptRequest,
15
+ PLUGIN_MODE,
16
+ } = require('./request-interceptor')
17
+ const {
18
+ sendMockResponse,
19
+ checkShouldCapture,
20
+ handleConcurrentCapture,
21
+ } = require('./response-handler')
22
+ const {
23
+ captureResponse,
24
+ } = require('./capture-handler')
25
+ const pluginModeManager = require('../plugin-mode-manager')
26
+
27
+ module.exports = (server, options) => {
28
+ console.log('[mockbubu] 🚀 server.js 模块已加载!')
29
+ const { storage } = options
30
+
31
+ // 使用 StorageAdapter 包装 Whistle storage
32
+ const actualBaseDir = options.config.baseDir
33
+ const storageAdapter = new StorageAdapter({ baseDir: actualBaseDir })
34
+
35
+ // 初始化组管理器(使用适配器)
36
+ const groupManager = new GroupManager(storageAdapter)
37
+
38
+ // 并发捕获控制
39
+ // Key: `${groupId}/${filename}`, Value: Promise (捕获完成时 resolve)
40
+ const capturingFiles = new Map()
41
+
42
+ // 同步初始化:确保初始化完成后再处理请求
43
+ let isInitialized = false
44
+ storageAdapter.init().then(async () => {
45
+ console.log('[mockbubu] ✅ V3 Storage 初始化完成')
46
+ // 确保默认组存在
47
+ await groupManager.ensureDefaultGroup()
48
+ // 初始化模式管理器
49
+ await pluginModeManager.init(storage)
50
+ isInitialized = true
51
+ }).catch(err => {
52
+ console.error('[mockbubu] ❌ V3 Storage 初始化失败:', err)
53
+ isInitialized = true // 即使失败也标记为已初始化,避免阻塞
54
+ })
55
+
56
+ server.on('request', async (req, res) => {
57
+ // 等待初始化完成
58
+ // eslint-disable-next-line no-unmodified-loop-condition
59
+ while (!isInitialized) {
60
+ await new Promise(resolve => setTimeout(resolve, 100))
61
+ }
62
+
63
+ // 判断是否需要拦截该请求(使用模式管理器获取当前模式)
64
+ const interceptResult = shouldInterceptRequest(req, storage, pluginModeManager.getMode())
65
+
66
+ // 不需要拦截,直接透传
67
+ if (!interceptResult.shouldIntercept) {
68
+ if (interceptResult.reason === 'not-json') {
69
+ console.log(`[mockbubu] ⏭️ 非 JSON 请求已跳过 | File: ${interceptResult.filename}`)
70
+ console.log(`[mockbubu] 📋 Headers: Accept=${interceptResult.headers.accept}, Sec-Fetch-Dest=${interceptResult.headers['sec-fetch-dest'] || interceptResult.headers['Sec-Fetch-Dest']}`)
71
+ }
72
+ return req.passThrough()
73
+ }
74
+
75
+ // 提取请求信息
76
+ const { filename, rule, method, url, pattern, ruleValue, mode } = interceptResult
77
+
78
+ try {
79
+ // 获取当前激活的组ID和文件配置
80
+ const currentGroupId = await groupManager.getCurrentGroupId()
81
+ const fileConfig = await groupManager.getGroupFileConfig(currentGroupId, filename)
82
+ const { mock, mockVersion } = fileConfig
83
+
84
+ // 从组目录读取源数据文件 (file.json)
85
+ const filePath = `${currentGroupId}/${filename}`
86
+ const sourceData = await storageAdapter.readFile(filePath)
87
+
88
+ // ⚠️ Mock-Only 模式特殊处理
89
+ if (mode === PLUGIN_MODE.MOCK_ONLY) {
90
+ // 如果没有开启 mock → 透传(不拦截,不捕获)
91
+ if (!mock) {
92
+ console.log(`[mockbubu] ⏭️ Mock-Only 模式: 文件未开启 Mock,透传 | URL: ${url}`)
93
+ return req.passThrough()
94
+ }
95
+
96
+ // 如果有 mock 但无数据 → 透传(不拦截,不捕获)
97
+ if (!sourceData) {
98
+ console.log(`[mockbubu] ⏭️ Mock-Only 模式: 文件无数据,透传 | URL: ${url}`)
99
+ return req.passThrough()
100
+ }
101
+
102
+ // 有 mock 且有数据 → 返回 mock(不捕获)
103
+ console.log(`[mockbubu] ✅ Mock-Only 模式: 返回 Mock 数据(不捕获) | URL: ${url}`)
104
+ return await sendMockResponse(res, sourceData, {
105
+ mockVersion,
106
+ groupManager,
107
+ currentGroupId,
108
+ filename,
109
+ })
110
+ }
111
+
112
+ // 以下是 Capture 模式的逻辑
113
+ console.log(`[mockbubu] 🎯 Capture 模式: JSON 请求,开始捕获 | URL: ${url}`)
114
+
115
+ // 场景 1: Mock 启用 + 有源数据 → 返回 Mock 数据
116
+ if (mock && sourceData) {
117
+ return await sendMockResponse(res, sourceData, {
118
+ mockVersion,
119
+ groupManager,
120
+ currentGroupId,
121
+ filename,
122
+ })
123
+ }
124
+
125
+ // 场景 2: Mock 启用 + 无源数据 → 容错恢复(自动捕获源数据)
126
+ // 可能原因:文件被删除、索引损坏、组导入不完整等
127
+ if (mock && !sourceData) {
128
+ const captureKey = `${currentGroupId}/${filename}`
129
+
130
+ // 检查文件是否已存在
131
+ const { shouldCapture, isFirstCapture } = await checkShouldCapture({
132
+ storageAdapter,
133
+ groupManager,
134
+ currentGroupId,
135
+ filename,
136
+ })
137
+
138
+ if (!shouldCapture) {
139
+ // 文件已存在,直接透传
140
+ return req.passThrough()
141
+ }
142
+
143
+ // 处理并发捕获
144
+ const concurrentResult = await handleConcurrentCapture(capturingFiles, captureKey, {
145
+ res,
146
+ storageAdapter,
147
+ groupManager,
148
+ currentGroupId,
149
+ filename,
150
+ mockVersion,
151
+ filePath,
152
+ })
153
+
154
+ if (concurrentResult.handled) {
155
+ if (concurrentResult.response === 'passthrough') {
156
+ return req.passThrough()
157
+ }
158
+ return // 已经返回响应
159
+ }
160
+
161
+ // 发起捕获
162
+ return await captureResponse({
163
+ req,
164
+ res,
165
+ storageAdapter,
166
+ groupManager,
167
+ memoryBuffer,
168
+ currentGroupId,
169
+ filename,
170
+ isFirstCapture,
171
+ mock: true, // mock 状态
172
+ requestInfo: { method, rule, pattern, ruleValue, url },
173
+ capturingFiles,
174
+ storage,
175
+ })
176
+ }
177
+
178
+ // 场景 3: Mock 未启用
179
+ if (!mock) {
180
+ // 3.1: 有源数据 → 直接透传
181
+ if (sourceData) {
182
+ return req.passThrough()
183
+ }
184
+
185
+ // 3.2: 无源数据 → 检查是否需要捕获
186
+ const captureKey = `${currentGroupId}/${filename}`
187
+
188
+ // 检查文件是否已存在
189
+ const { shouldCapture, isFirstCapture } = await checkShouldCapture({
190
+ storageAdapter,
191
+ groupManager,
192
+ currentGroupId,
193
+ filename,
194
+ })
195
+
196
+ if (!shouldCapture) {
197
+ // 文件已存在,直接透传
198
+ console.log(`[mockbubu] ⏭️ 文件已存在(mock=false),跳过捕获并透传: ${filename}`)
199
+ return req.passThrough()
200
+ }
201
+
202
+ // 检查是否正在捕获中
203
+ if (capturingFiles.has(captureKey)) {
204
+ // mock=false 时,并发请求直接透传(不等待)
205
+ console.log(`[mockbubu] ⏳ 检测到并发请求(mock=false),直接透传: ${filename}`)
206
+ return req.passThrough()
207
+ }
208
+
209
+ // 发起捕获
210
+ return await captureResponse({
211
+ req,
212
+ res,
213
+ storageAdapter,
214
+ groupManager,
215
+ memoryBuffer,
216
+ currentGroupId,
217
+ filename,
218
+ isFirstCapture,
219
+ mock: false, // mock 状态
220
+ requestInfo: { method, rule, pattern, ruleValue, url },
221
+ capturingFiles,
222
+ storage,
223
+ })
224
+ }
225
+ } catch (error) {
226
+ console.error('[mockbubu] ❌ 请求处理失败:', error)
227
+ req.passThrough()
228
+ }
229
+ })
230
+ }