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/.gitignore +4 -0
- package/CHANGELOG_GROUP_FEATURE.md +468 -0
- package/CHANGELOG_P0_FIXES.md +412 -0
- package/CHANGELOG_P1_OPTIMIZATIONS.md +292 -0
- package/CLAUDE.md +436 -0
- package/GROUP_FEATURE_DESIGN.md +520 -0
- package/lib/const.js +19 -0
- package/lib/group-manager.js +491 -0
- package/lib/server.js +173 -33
- package/lib/uiServer/index.js +46 -4
- package/lib/uiServer/router/group-router.js +218 -0
- package/lib/uiServer/router/index.js +1393 -38
- package/lib/uiServer/router/version-router.js +131 -53
- package/lib/uiServer/util.js +64 -10
- package/lib/uiServer/validator.js +105 -0
- package/lib/utils.js +149 -27
- package/package.json +1 -1
- package/public/js/app.js +4541 -747
- package/public/js/app.js.map +1 -1
- package/public/js/chunk-vendors.js +12194 -6869
- package/public/js/chunk-vendors.js.map +1 -1
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))
|
|
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
|
-
|
|
38
|
-
const
|
|
46
|
+
// 获取当前激活的组ID
|
|
47
|
+
const currentGroupId = groupManager.getCurrentGroupId()
|
|
48
|
+
// 读取该组的配置
|
|
49
|
+
const groupConfig = groupManager.getGroupFileConfig(currentGroupId, filename)
|
|
50
|
+
const { mock, mockVersion } = groupConfig
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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)
|
|
165
|
+
// mock: false,已有缓存则直接透传,无缓存则捕获
|
|
166
|
+
if (sessionCache) {
|
|
167
|
+
console.log(`[mockbubu] 📤 直接透传(已有缓存): ${filename}`)
|
|
168
|
+
return req.passThrough()
|
|
169
|
+
}
|
|
59
170
|
|
|
60
|
-
//
|
|
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
|
-
|
|
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
|
})
|
package/lib/uiServer/index.js
CHANGED
|
@@ -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: '
|
|
28
|
-
formLimit: '
|
|
29
|
-
textLimit: '
|
|
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
|
+
}
|