whistle.mockbubu 1.0.0-dev.4 → 2.0.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/lib/server.js CHANGED
@@ -1,17 +1,22 @@
1
1
  const {
2
- getProperty,
3
2
  setProperty,
4
3
  getFilename,
5
4
  getRule,
6
5
  isJsonReq,
7
6
  setApiListUpdated,
8
- getVersionContent,
9
7
  handleBuffer2String,
10
8
  withTryCatch,
11
9
  } = require('./utils')
12
10
  const qs = require('qs')
11
+ const GroupManager = require('./group-manager')
13
12
 
14
13
  module.exports = (server, { storage }) => {
14
+ // 初始化组管理器
15
+ const groupManager = new GroupManager(storage)
16
+
17
+ // 首次启动时迁移现有数据到默认组
18
+ groupManager.migrateExistingData()
19
+
15
20
  server.on('request', (req, res) => {
16
21
  const { originalReq, method } = req
17
22
  const { headers, ruleValue, url, pattern } = originalReq
@@ -19,9 +24,13 @@ module.exports = (server, { storage }) => {
19
24
  const rule = getRule(originalReq)
20
25
 
21
26
  // 非json请求直接透传
22
- if (!isJsonReq(headers)) return req.passThrough()
27
+ if (!isJsonReq(headers)) {
28
+ console.log(`[mockbubu] ⏭️ 非 JSON 请求已跳过 | File: ${filename}`)
29
+ console.log(`[mockbubu] 📋 Headers: Accept=${headers.accept}, Sec-Fetch-Dest=${headers['sec-fetch-dest'] || headers['Sec-Fetch-Dest']}`)
30
+ return req.passThrough()
31
+ }
23
32
 
24
- // 获取请求信息
33
+ // 获取请求信息 - 这些写入到全局 properties
25
34
  let str = ''
26
35
  req.on('data', (chunk) => {
27
36
  str += chunk
@@ -34,30 +43,133 @@ module.exports = (server, { storage }) => {
34
43
  })
35
44
 
36
45
  try {
37
- const { mock, mockVersion } = getProperty(storage, filename) || {}
38
- const sessionCache = storage.readFile(filename)
46
+ // 获取当前激活的组ID
47
+ const currentGroupId = groupManager.getCurrentGroupId()
48
+ // 读取该组的配置
49
+ const groupConfig = groupManager.getGroupFileConfig(currentGroupId, filename)
50
+ const { mock, mockVersion } = groupConfig
39
51
 
40
- if (mock) {
41
- if (sessionCache) {
42
- const resCache = JSON.parse(sessionCache)?.res
43
- const { statusCode, statusMessage, headers } = resCache
44
-
45
- headers['from-res-cache'] = 'true'
46
- delete headers['content-encoding']
47
- delete headers['content-length']
48
- res.writeHead(statusCode, statusMessage, headers)
49
- if (mockVersion) {
50
- const mockVersionContent = getVersionContent({ storage, filename, versionName: mockVersion })
52
+ // 添加调试日志 - 使用一行显示关键信息
53
+ console.log(`[mockbubu] 🔍 REQUEST | File: ${filename} | Group: ${currentGroupId} | Mock: ${mock} | Version: ${mockVersion || 'source'}`)
54
+ console.log('[mockbubu] 📝 Full Config:', JSON.stringify(groupConfig, null, 2))
55
+
56
+ // 从组目录读取缓存文件
57
+ const filePath = `${currentGroupId}/${filename}`
58
+ const sessionCache = storage.readFile(filePath)
59
+ console.log(`[mockbubu] 💾 Cache Status: ${sessionCache ? 'EXISTS' : 'NO CACHE'} | Path: ${filePath}`)
60
+
61
+ if (mock && sessionCache) {
62
+ // Mock: true 且有缓存,返回模拟数据
63
+ const resCache = JSON.parse(sessionCache)?.res
64
+ const { statusCode, statusMessage, headers } = resCache
65
+
66
+ headers['from-res-cache'] = 'true'
67
+ delete headers['content-encoding']
68
+ delete headers['content-length']
69
+ res.writeHead(statusCode, statusMessage, headers)
70
+
71
+ if (mockVersion) {
72
+ // 从当前组配置读取版本内容
73
+ console.log(`[mockbubu] ⚡ 使用版本数据 | Version: ${mockVersion}`)
74
+ const mockVersionContent = groupManager.getGroupVersionContent(currentGroupId, filename, mockVersion)
75
+ if (mockVersionContent) {
76
+ console.log(`[mockbubu] ✅ 版本数据存在,返回版本: ${mockVersion}`)
51
77
  res.end(JSON.stringify(mockVersionContent))
52
78
  } else {
79
+ // 版本不存在时降级到原始数据
80
+ console.log(`[mockbubu] ⚠️ 版本 ${mockVersion} 不存在,降级到原始数据`)
53
81
  res.end(resCache.body)
54
82
  }
83
+ } else {
84
+ console.log('[mockbubu] ⚡ 使用原始数据 (无版本)')
85
+ res.end(resCache.body)
55
86
  }
87
+ } else if (mock && !sessionCache) {
88
+ // Mock: true 但无缓存,需要先捕获数据
89
+ console.log(`[mockbubu] ⚠️ Mock enabled but no cache for ${filename}, need to capture first`)
90
+ // 不要 return,继续往下执行捕获逻辑
91
+ // 无缓存,捕获最新的接口数据
92
+ console.log(`[mockbubu] 🎣 开始捕获(无缓存): ${filename}`)
93
+ const client = req.request((svrRes) => {
94
+ const encoding = svrRes.headers['content-encoding']
95
+ let body
96
+
97
+ svrRes.on('data', (data) => {
98
+ body = body ? Buffer.concat([body, data]) : data
99
+ })
100
+ svrRes.on('end', withTryCatch(async () => {
101
+ if (!body) return
102
+
103
+ const content = await handleBuffer2String({ body, encoding })
104
+ // 获取完整的抓包数据,要等待响应完成
105
+ req.getSession(async (session) => {
106
+ // 如果设置了 enable://hide 会获取到空数据
107
+ if (!session) {
108
+ return
109
+ }
110
+
111
+ // 完全隔离架构:将元数据和配置写入当前组
112
+ // 检查组配置是否真实存在(而不是默认返回值)
113
+ const configKey = groupManager.getGroupConfigKey(currentGroupId, filename)
114
+ const isFirstCapture = !storage.hasProperty(configKey)
115
+
116
+ if (isFirstCapture) {
117
+ // 首次捕获:创建完整配置(保持当前 mock 状态)
118
+ groupManager.setGroupFileConfig(currentGroupId, filename, {
119
+ method,
120
+ rule,
121
+ status: session.res.statusCode,
122
+ pattern,
123
+ ruleValue: ruleValue || 'pathname',
124
+ url,
125
+ date: Date.now(),
126
+ mock, // 使用当前的 mock 状态(true)
127
+ locked: false,
128
+ mockVersion: null,
129
+ mockTime: null,
130
+ })
131
+ console.log(`[mockbubu] ✅ 首次捕获文件到组 ${currentGroupId}: ${filename} | Mock: ${mock}`)
132
+ } else {
133
+ // 已存在配置,仅更新元数据(不覆盖 mock、locked 等用户配置)
134
+ const existingGroupConfig = groupManager.getGroupFileConfig(currentGroupId, filename)
135
+ groupManager.setGroupFileConfig(currentGroupId, filename, {
136
+ ...existingGroupConfig,
137
+ method,
138
+ rule,
139
+ status: session.res.statusCode,
140
+ pattern,
141
+ ruleValue: ruleValue || 'pathname',
142
+ url,
143
+ date: Date.now(),
144
+ })
145
+ console.log(`[mockbubu] 🔄 更新文件元数据到组 ${currentGroupId}: ${filename}`)
146
+ }
147
+
148
+ // 将文件写入组目录
149
+ const tempSession = JSON.parse(JSON.stringify(session))
150
+ tempSession.res.body = content
151
+ const groupFilePath = `${currentGroupId}/${filename}`
152
+ storage.writeFile(groupFilePath, JSON.stringify(tempSession))
153
+ console.log(`[mockbubu] 💾 写入文件: ${groupFilePath}`)
154
+
155
+ setApiListUpdated(storage, true)
156
+ })
157
+ }))
158
+
159
+ // 将响应透传给客户端
160
+ svrRes.pipe(res)
161
+ })
162
+
163
+ req.pipe(client)
56
164
  } else {
57
- // 已有缓存则直接透传
58
- if (sessionCache) return req.passThrough()
165
+ // mock: false,已有缓存则直接透传,无缓存则捕获
166
+ if (sessionCache) {
167
+ console.log(`[mockbubu] 📤 直接透传(已有缓存): ${filename}`)
168
+ return req.passThrough()
169
+ }
59
170
 
60
- // 命中插件规则,没有勾选mock的缓存最新的接口数据
171
+ // 无缓存,捕获最新的接口数据
172
+ console.log(`[mockbubu] 🎣 开始捕获(无缓存): ${filename}`)
61
173
  const client = req.request((svrRes) => {
62
174
  const encoding = svrRes.headers['content-encoding']
63
175
  let body
@@ -75,22 +187,50 @@ module.exports = (server, { storage }) => {
75
187
  if (!session) {
76
188
  return
77
189
  }
78
- setProperty(storage, filename, {
79
- method,
80
- rule,
81
- status: session.res.statusCode,
82
- pattern,
83
- ruleValue: ruleValue || 'pathname',
84
- url,
85
- mock: false,
86
- locked: false,
87
- date: Date.now(),
88
- mockTime: null,
89
- })
90
190
 
191
+ // 完全隔离架构:将元数据和配置写入当前组
192
+ // 检查组配置是否真实存在(而不是默认返回值)
193
+ const configKey = groupManager.getGroupConfigKey(currentGroupId, filename)
194
+ const isFirstCapture = !storage.hasProperty(configKey)
195
+
196
+ if (isFirstCapture) {
197
+ // 首次捕获:创建完整配置
198
+ groupManager.setGroupFileConfig(currentGroupId, filename, {
199
+ method,
200
+ rule,
201
+ status: session.res.statusCode,
202
+ pattern,
203
+ ruleValue: ruleValue || 'pathname',
204
+ url,
205
+ date: Date.now(),
206
+ mock: false,
207
+ locked: false,
208
+ mockVersion: null,
209
+ mockTime: null,
210
+ })
211
+ console.log(`[mockbubu] ✅ 首次捕获文件到组 ${currentGroupId}: ${filename}`)
212
+ } else {
213
+ // 已存在配置,仅更新元数据(不覆盖 mock、locked 等用户配置)
214
+ const existingGroupConfig = groupManager.getGroupFileConfig(currentGroupId, filename)
215
+ groupManager.setGroupFileConfig(currentGroupId, filename, {
216
+ ...existingGroupConfig,
217
+ method,
218
+ rule,
219
+ status: session.res.statusCode,
220
+ pattern,
221
+ ruleValue: ruleValue || 'pathname',
222
+ url,
223
+ date: Date.now(),
224
+ })
225
+ console.log(`[mockbubu] 🔄 更新文件元数据到组 ${currentGroupId}: ${filename}`)
226
+ }
227
+
228
+ // 将文件写入组目录
91
229
  const tempSession = JSON.parse(JSON.stringify(session))
92
230
  tempSession.res.body = content
93
- storage.writeFile(filename, JSON.stringify(tempSession))
231
+ const groupFilePath = `${currentGroupId}/${filename}`
232
+ storage.writeFile(groupFilePath, JSON.stringify(tempSession))
233
+ console.log(`[mockbubu] 💾 写入文件: ${groupFilePath}`)
94
234
 
95
235
  setApiListUpdated(storage, true)
96
236
  })
@@ -6,10 +6,41 @@ const path = require('path')
6
6
  const router = require('koa-router')()
7
7
  const setupRouter = require('./router')
8
8
  const cors = require('koa2-cors')
9
+ const GroupManager = require('../group-manager')
9
10
  const MAX_AGE = 1000 * 60 * 5
10
11
 
11
12
  module.exports = (server, options) => {
12
13
  const app = new Koa()
14
+ const { storage } = options
15
+
16
+ console.log('[mockbubu] 插件开始初始化...')
17
+
18
+ // 初始化组管理器
19
+ let groupManager
20
+ try {
21
+ console.log('[mockbubu] 创建 GroupManager...')
22
+ groupManager = new GroupManager(storage)
23
+ console.log('[mockbubu] GroupManager 创建成功')
24
+
25
+ // 首次启动时迁移现有数据到默认组(已添加大文件保护)
26
+ console.log('[mockbubu] 开始数据迁移...')
27
+ groupManager.migrateExistingData()
28
+ console.log('[mockbubu] 数据迁移完成')
29
+ } catch (error) {
30
+ console.error('[mockbubu] 初始化失败:', error)
31
+ console.error('[mockbubu] 错误堆栈:', error.stack)
32
+ // 如果初始化失败,创建一个简单的 mock 对象
33
+ groupManager = {
34
+ getCurrentGroupId: () => 'default',
35
+ getCurrentGroup: () => ({ id: 'default', name: '默认组' }),
36
+ getGroups: () => [{ id: 'default', name: '默认组' }],
37
+ }
38
+ }
39
+
40
+ // 将 groupManager 挂载到 app.context,让所有中间件都能访问
41
+ app.context.groupManager = groupManager
42
+
43
+ console.log('[mockbubu] 插件初始化完成')
13
44
 
14
45
  app.proxy = true
15
46
  app.silent = true
@@ -17,16 +48,27 @@ module.exports = (server, options) => {
17
48
  setupRouter(router, options)
18
49
  app.use(
19
50
  cors({
20
- origin: '*',
51
+ origin: (ctx) => {
52
+ const origin = ctx.get('Origin')
53
+ // 只允许 localhost 和 127.0.0.1 的请求
54
+ if (!origin || /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(origin)) {
55
+ return origin || '*'
56
+ }
57
+ // Whistle UI 的域名
58
+ if (/^https?:\/\/local\.whistlejs\.com(:\d+)?$/i.test(origin)) {
59
+ return origin
60
+ }
61
+ return false
62
+ },
21
63
  maxAge: 10,
22
64
  credentials: true,
23
65
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
24
66
  }),
25
67
  )
26
68
  app.use(bodyParser({
27
- jsonLimit: '10mb',
28
- formLimit: '10mb',
29
- textLimit: '10mb',
69
+ jsonLimit: '100mb',
70
+ formLimit: '100mb',
71
+ textLimit: '100mb',
30
72
  }))
31
73
  app.use(router.routes())
32
74
  app.use(router.allowedMethods())
@@ -0,0 +1,218 @@
1
+ const {
2
+ createErrorResponse,
3
+ createSuccessResponse,
4
+ } = require('../util')
5
+ const {
6
+ validate,
7
+ } = require('../validator')
8
+
9
+ /**
10
+ * 组管理路由
11
+ */
12
+ module.exports = (router) => {
13
+ // 获取组列表
14
+ router.post('/cgi-bin/mockbubu/groups/list', (ctx) => {
15
+ try {
16
+ const { groupManager } = ctx
17
+ const groups = groupManager.getGroups()
18
+ const currentGroupId = groupManager.getCurrentGroupId()
19
+
20
+ ctx.body = createSuccessResponse({
21
+ groups,
22
+ currentGroupId,
23
+ })
24
+ } catch (error) {
25
+ ctx.body = createErrorResponse(error.message)
26
+ }
27
+ })
28
+
29
+ // 获取当前组
30
+ router.post('/cgi-bin/mockbubu/groups/current', (ctx) => {
31
+ try {
32
+ const { groupManager } = ctx
33
+ const currentGroup = groupManager.getCurrentGroup()
34
+
35
+ ctx.body = createSuccessResponse({
36
+ currentGroup,
37
+ currentGroupId: groupManager.getCurrentGroupId(),
38
+ })
39
+ } catch (error) {
40
+ ctx.body = createErrorResponse(error.message)
41
+ }
42
+ })
43
+
44
+ // 创建组
45
+ router.post('/cgi-bin/mockbubu/groups/create', validate({
46
+ name: (val) => {
47
+ if (!val || !val.trim()) {
48
+ throw new Error('组名不能为空')
49
+ }
50
+ if (val.trim().length > 50) {
51
+ throw new Error('组名长度不能超过50个字符')
52
+ }
53
+ return null
54
+ },
55
+ description: (val) => {
56
+ if (val && val.length > 200) {
57
+ throw new Error('描述长度不能超过200个字符')
58
+ }
59
+ return null
60
+ },
61
+ copyFromGroupId: (val) => {
62
+ // 可选参数,如果提供则必须是字符串
63
+ if (val !== undefined && val !== null && typeof val !== 'string') {
64
+ throw new Error('copyFromGroupId 必须是字符串')
65
+ }
66
+ return null
67
+ },
68
+ }), (ctx) => {
69
+ try {
70
+ const { groupManager } = ctx
71
+ const { name, description, copyFromGroupId } = ctx.request.body
72
+
73
+ const newGroup = groupManager.createGroup({
74
+ name: name.trim(),
75
+ description: description ? description.trim() : '',
76
+ copyFromGroupId: copyFromGroupId || null,
77
+ })
78
+
79
+ ctx.body = createSuccessResponse(newGroup, '创建成功')
80
+ } catch (error) {
81
+ ctx.body = createErrorResponse(error.message)
82
+ }
83
+ })
84
+
85
+ // 更新组
86
+ router.post('/cgi-bin/mockbubu/groups/update', validate({
87
+ groupId: (val) => {
88
+ if (!val || typeof val !== 'string') {
89
+ throw new Error('groupId 必须是字符串')
90
+ }
91
+ return null
92
+ },
93
+ name: (val) => {
94
+ if (val !== undefined && val !== null) {
95
+ if (!val.trim()) {
96
+ throw new Error('组名不能为空')
97
+ }
98
+ if (val.trim().length > 50) {
99
+ throw new Error('组名长度不能超过50个字符')
100
+ }
101
+ }
102
+ return null
103
+ },
104
+ description: (val) => {
105
+ if (val !== undefined && val !== null && val.length > 200) {
106
+ throw new Error('描述长度不能超过200个字符')
107
+ }
108
+ return null
109
+ },
110
+ }), (ctx) => {
111
+ try {
112
+ const { groupManager } = ctx
113
+ const { groupId, name, description } = ctx.request.body
114
+
115
+ const updatedGroup = groupManager.updateGroup(groupId, {
116
+ name: name ? name.trim() : undefined,
117
+ description: description !== undefined ? description.trim() : undefined,
118
+ })
119
+
120
+ ctx.body = createSuccessResponse(updatedGroup, '更新成功')
121
+ } catch (error) {
122
+ ctx.body = createErrorResponse(error.message)
123
+ }
124
+ })
125
+
126
+ // 删除组
127
+ router.post('/cgi-bin/mockbubu/groups/delete', validate({
128
+ groupId: (val) => {
129
+ if (!val || typeof val !== 'string') {
130
+ throw new Error('groupId 必须是字符串')
131
+ }
132
+ return null
133
+ },
134
+ }), (ctx) => {
135
+ try {
136
+ const { groupManager } = ctx
137
+ const { groupId } = ctx.request.body
138
+
139
+ groupManager.deleteGroup(groupId)
140
+
141
+ ctx.body = createSuccessResponse(null, '删除成功')
142
+ } catch (error) {
143
+ ctx.body = createErrorResponse(error.message)
144
+ }
145
+ })
146
+
147
+ // 切换当前组
148
+ router.post('/cgi-bin/mockbubu/groups/switch', validate({
149
+ groupId: (val) => {
150
+ if (!val || typeof val !== 'string') {
151
+ throw new Error('groupId 必须是字符串')
152
+ }
153
+ return null
154
+ },
155
+ }), (ctx) => {
156
+ try {
157
+ const { groupManager } = ctx
158
+ const { groupId } = ctx.request.body
159
+
160
+ const currentGroup = groupManager.setCurrentGroup(groupId)
161
+
162
+ ctx.body = createSuccessResponse({
163
+ currentGroup,
164
+ currentGroupId: groupId,
165
+ }, '切换成功')
166
+ } catch (error) {
167
+ ctx.body = createErrorResponse(error.message)
168
+ }
169
+ })
170
+
171
+ // 复制组
172
+ router.post('/cgi-bin/mockbubu/groups/copy', validate({
173
+ sourceGroupId: (val) => {
174
+ if (!val || typeof val !== 'string') {
175
+ throw new Error('sourceGroupId 必须是字符串')
176
+ }
177
+ return null
178
+ },
179
+ name: (val) => {
180
+ if (!val || !val.trim()) {
181
+ throw new Error('组名不能为空')
182
+ }
183
+ if (val.trim().length > 50) {
184
+ throw new Error('组名长度不能超过50个字符')
185
+ }
186
+ return null
187
+ },
188
+ description: (val) => {
189
+ if (val && val.length > 200) {
190
+ throw new Error('描述长度不能超过200个字符')
191
+ }
192
+ return null
193
+ },
194
+ }), (ctx) => {
195
+ try {
196
+ const { groupManager } = ctx
197
+ const { sourceGroupId, name, description } = ctx.request.body
198
+
199
+ // 检查源组是否存在
200
+ const groups = groupManager.getGroups()
201
+ const sourceGroup = groups.find(g => g.id === sourceGroupId)
202
+ if (!sourceGroup) {
203
+ throw new Error(`源组不存在: ${sourceGroupId}`)
204
+ }
205
+
206
+ // 创建新组并复制配置
207
+ const newGroup = groupManager.createGroup({
208
+ name: name.trim(),
209
+ description: description ? description.trim() : '',
210
+ copyFromGroupId: sourceGroupId,
211
+ })
212
+
213
+ ctx.body = createSuccessResponse(newGroup, '复制成功')
214
+ } catch (error) {
215
+ ctx.body = createErrorResponse(error.message)
216
+ }
217
+ })
218
+ }