whistle.mockbubu 1.0.0-dev.4 → 2.0.0-beta.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 +469 -0
- package/GROUP_FEATURE_DESIGN.md +520 -0
- package/README.md +106 -0
- package/lib/archive-utils.js +332 -0
- package/lib/const.js +19 -0
- package/lib/group-manager.js +660 -0
- package/lib/migration-v3.js +321 -0
- package/lib/server.js +333 -60
- package/lib/storage-adapter.js +518 -0
- package/lib/storage-v3.js +1368 -0
- package/lib/uiServer/index.js +76 -5
- package/lib/uiServer/router/group-router.js +218 -0
- package/lib/uiServer/router/index.js +1074 -51
- package/lib/uiServer/router/version-router.js +208 -63
- package/lib/uiServer/util.js +74 -16
- package/lib/uiServer/validator.js +105 -0
- package/lib/utils.js +107 -171
- package/package.json +1 -1
- package/public/js/app.js +5216 -1379
- package/public/js/app.js.map +1 -1
- package/public/js/chunk-vendors.js +14179 -8217
- package/public/js/chunk-vendors.js.map +1 -1
- package/rules.txt +1 -1
- package//346/212/200/346/234/257/346/226/271/346/241/210.md +452 -0
- package//346/265/213/350/257/225/346/215/225/350/216/267/345/212/237/350/203/275/346/255/245/351/252/244.md +145 -0
package/lib/uiServer/index.js
CHANGED
|
@@ -6,27 +6,98 @@ 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')
|
|
10
|
+
const StorageAdapter = require('../storage-adapter')
|
|
9
11
|
const MAX_AGE = 1000 * 60 * 5
|
|
10
12
|
|
|
11
13
|
module.exports = (server, options) => {
|
|
12
14
|
const app = new Koa()
|
|
15
|
+
const { storage } = options
|
|
16
|
+
|
|
17
|
+
console.log('[mockbubu] 插件开始初始化...')
|
|
18
|
+
|
|
19
|
+
// 使用 StorageAdapter 包装 Whistle storage
|
|
20
|
+
// Whistle storage 对象直接有 baseDir 属性
|
|
21
|
+
const actualBaseDir = storage.baseDir || storage._options?.baseDir
|
|
22
|
+
console.log('[mockbubu] 🔍 UI Server storage.baseDir:', storage.baseDir)
|
|
23
|
+
console.log('[mockbubu] 🔍 UI Server 使用 baseDir:', actualBaseDir)
|
|
24
|
+
const storageAdapter = new StorageAdapter({ baseDir: actualBaseDir })
|
|
25
|
+
|
|
26
|
+
// 同步初始化:确保初始化完成后再处理请求
|
|
27
|
+
let isInitialized = false
|
|
28
|
+
let groupManager
|
|
29
|
+
|
|
30
|
+
;(async () => {
|
|
31
|
+
try {
|
|
32
|
+
// 等待 storage 初始化完成
|
|
33
|
+
await storageAdapter.init()
|
|
34
|
+
console.log('[mockbubu] ✅ V3 Storage (UI Server) 已初始化')
|
|
35
|
+
|
|
36
|
+
// 创建组管理器
|
|
37
|
+
console.log('[mockbubu] 创建 GroupManager...')
|
|
38
|
+
groupManager = new GroupManager(storageAdapter)
|
|
39
|
+
console.log('[mockbubu] GroupManager 创建成功')
|
|
40
|
+
|
|
41
|
+
// 确保默认组存在
|
|
42
|
+
console.log('[mockbubu] 确保默认组存在...')
|
|
43
|
+
await groupManager.ensureDefaultGroup()
|
|
44
|
+
console.log('[mockbubu] 默认组已准备就绪')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
isInitialized = true
|
|
48
|
+
console.log('[mockbubu] 插件初始化完成')
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('[mockbubu] 初始化失败:', error)
|
|
51
|
+
console.error('[mockbubu] 错误堆栈:', error.stack)
|
|
52
|
+
// 如果初始化失败,创建一个简单的 mock 对象
|
|
53
|
+
groupManager = {
|
|
54
|
+
getCurrentGroupId: async () => 'default',
|
|
55
|
+
getCurrentGroup: async () => ({ id: 'default', name: '默认组' }),
|
|
56
|
+
getGroups: async () => [{ id: 'default', name: '默认组' }],
|
|
57
|
+
}
|
|
58
|
+
isInitialized = true
|
|
59
|
+
}
|
|
60
|
+
})()
|
|
61
|
+
|
|
62
|
+
// 中间件:等待初始化完成
|
|
63
|
+
app.use(async (ctx, next) => {
|
|
64
|
+
// eslint-disable-next-line no-unmodified-loop-condition
|
|
65
|
+
while (!isInitialized) {
|
|
66
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
67
|
+
}
|
|
68
|
+
// 将 groupManager 和 storageAdapter 挂载到 ctx,让所有路由都能访问
|
|
69
|
+
ctx.groupManager = groupManager
|
|
70
|
+
ctx.storageAdapter = storageAdapter
|
|
71
|
+
await next()
|
|
72
|
+
})
|
|
13
73
|
|
|
14
74
|
app.proxy = true
|
|
15
75
|
app.silent = true
|
|
16
76
|
onerror(app)
|
|
17
|
-
setupRouter(router, options)
|
|
77
|
+
setupRouter(router, { ...options, storage: storageAdapter })
|
|
18
78
|
app.use(
|
|
19
79
|
cors({
|
|
20
|
-
origin:
|
|
80
|
+
origin: (ctx) => {
|
|
81
|
+
const origin = ctx.get('Origin')
|
|
82
|
+
// 只允许 localhost 和 127.0.0.1 的请求
|
|
83
|
+
if (!origin || /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(origin)) {
|
|
84
|
+
return origin || '*'
|
|
85
|
+
}
|
|
86
|
+
// Whistle UI 的域名
|
|
87
|
+
if (/^https?:\/\/local\.whistlejs\.com(:\d+)?$/i.test(origin)) {
|
|
88
|
+
return origin
|
|
89
|
+
}
|
|
90
|
+
return false
|
|
91
|
+
},
|
|
21
92
|
maxAge: 10,
|
|
22
93
|
credentials: true,
|
|
23
94
|
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
24
95
|
}),
|
|
25
96
|
)
|
|
26
97
|
app.use(bodyParser({
|
|
27
|
-
jsonLimit: '
|
|
28
|
-
formLimit: '
|
|
29
|
-
textLimit: '
|
|
98
|
+
jsonLimit: '100mb',
|
|
99
|
+
formLimit: '100mb',
|
|
100
|
+
textLimit: '100mb',
|
|
30
101
|
}))
|
|
31
102
|
app.use(router.routes())
|
|
32
103
|
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', async (ctx) => {
|
|
15
|
+
try {
|
|
16
|
+
const { groupManager } = ctx
|
|
17
|
+
const groups = await groupManager.getGroups()
|
|
18
|
+
const currentGroupId = await 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', async (ctx) => {
|
|
31
|
+
try {
|
|
32
|
+
const { groupManager } = ctx
|
|
33
|
+
const currentGroup = await groupManager.getCurrentGroup()
|
|
34
|
+
|
|
35
|
+
ctx.body = createSuccessResponse({
|
|
36
|
+
currentGroup,
|
|
37
|
+
currentGroupId: await 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
|
+
}), async (ctx) => {
|
|
69
|
+
try {
|
|
70
|
+
const { groupManager } = ctx
|
|
71
|
+
const { name, description, copyFromGroupId } = ctx.request.body
|
|
72
|
+
|
|
73
|
+
const newGroup = await 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
|
+
}), async (ctx) => {
|
|
111
|
+
try {
|
|
112
|
+
const { groupManager } = ctx
|
|
113
|
+
const { groupId, name, description } = ctx.request.body
|
|
114
|
+
|
|
115
|
+
const updatedGroup = await 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
|
+
}), async (ctx) => {
|
|
135
|
+
try {
|
|
136
|
+
const { groupManager } = ctx
|
|
137
|
+
const { groupId } = ctx.request.body
|
|
138
|
+
|
|
139
|
+
await 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
|
+
}), async (ctx) => {
|
|
156
|
+
try {
|
|
157
|
+
const { groupManager } = ctx
|
|
158
|
+
const { groupId } = ctx.request.body
|
|
159
|
+
|
|
160
|
+
const currentGroup = await 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
|
+
}), async (ctx) => {
|
|
195
|
+
try {
|
|
196
|
+
const { groupManager } = ctx
|
|
197
|
+
const { sourceGroupId, name, description } = ctx.request.body
|
|
198
|
+
|
|
199
|
+
// 检查源组是否存在
|
|
200
|
+
const groups = await 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 = await 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
|
+
}
|