whistle.mockbubu 2.1.5 → 2.2.0-beta.2
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/config/const.js +11 -0
- package/lib/core/server-entry/response-handler.js +210 -7
- package/lib/core/server-entry/server.js +33 -33
- package/lib/storage/group-manager.js +77 -0
- package/lib/storage/storage-v3.js +160 -0
- package/lib/uiServer/router/files/api-list-router.js +1 -2
- package/lib/uiServer/router/files/file-config-router.js +23 -1
- package/lib/uiServer/router/files/file-save-router.js +39 -4
- package/lib/uiServer/router/files/match-version-router.js +178 -0
- package/lib/uiServer/router/index.js +2 -0
- package/lib/uiServer/utils/util.js +25 -5
- package/package.json +1 -1
- package/public/js/app.js +2011 -299
- package/public/js/app.js.map +1 -1
- package/public/js/chunk-vendors.js +650 -0
- package/public/js/chunk-vendors.js.map +1 -1
- package/public/js/node_modules_element-ui_lib_element-ui_common_js.js +17713 -0
- package/public/js/node_modules_element-ui_lib_element-ui_common_js.js.map +1 -0
|
@@ -18,7 +18,7 @@ const {
|
|
|
18
18
|
validateBoolean,
|
|
19
19
|
validate,
|
|
20
20
|
} = require('../../utils/validator')
|
|
21
|
-
const { wrapRouteHandler } = require('../../utils/router-helpers')
|
|
21
|
+
const { wrapRouteHandler, getFileIdByUrl } = require('../../utils/router-helpers')
|
|
22
22
|
|
|
23
23
|
module.exports = (router) => {
|
|
24
24
|
/**
|
|
@@ -115,4 +115,26 @@ module.exports = (router) => {
|
|
|
115
115
|
|
|
116
116
|
ctx.body = createSuccessResponse(null, '更新成功')
|
|
117
117
|
}, '更新锁定开关'))
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 更新备注
|
|
121
|
+
*
|
|
122
|
+
* 请求体:
|
|
123
|
+
* { name: 'https://api.example.com/users', remark: '用户列表接口' }
|
|
124
|
+
*/
|
|
125
|
+
router.post('/cgi-bin/mockbubu/update-api-remark', validate({
|
|
126
|
+
name: validateFilename,
|
|
127
|
+
remark: (val) => {
|
|
128
|
+
if (typeof val !== 'string') return 'remark 必须是字符串'
|
|
129
|
+
if (val.length > 200) return 'remark 不能超过 200 字'
|
|
130
|
+
return null
|
|
131
|
+
},
|
|
132
|
+
}), wrapRouteHandler(async (ctx) => {
|
|
133
|
+
const { name, remark } = ctx.request.body
|
|
134
|
+
const { fileId, currentGroupId, v3Storage } = await getFileIdByUrl(ctx, name)
|
|
135
|
+
|
|
136
|
+
await v3Storage.updateFile(currentGroupId, fileId, { remark })
|
|
137
|
+
|
|
138
|
+
ctx.body = createSuccessResponse(null, '更新成功')
|
|
139
|
+
}, '更新备注'))
|
|
118
140
|
}
|
|
@@ -76,11 +76,46 @@ module.exports = (router) => {
|
|
|
76
76
|
|
|
77
77
|
console.log(`${LOG_PREFIX.FILE_SAVE} 检查文件是否存在: ${filename}, 结果:`, existingFileEntry ? '已存在' : '不存在')
|
|
78
78
|
|
|
79
|
-
//
|
|
79
|
+
// 如果索引中已有记录,验证物理文件是否真实存在
|
|
80
80
|
if (existingFileEntry) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
const existingFile = await v3Storage.getFile(currentGroupId, existingFileEntry.id)
|
|
82
|
+
|
|
83
|
+
if (existingFile) {
|
|
84
|
+
// 物理文件存在:同步更新 config 并返回文件信息(含 id)
|
|
85
|
+
console.log(`${LOG_PREFIX.FILE_SAVE} 文件已存在,同步 config 并返回文件信息: ${filename}`)
|
|
86
|
+
const configToSet = {
|
|
87
|
+
mock: config?.mock !== undefined ? config.mock : (existingFile.config?.mock || false),
|
|
88
|
+
locked: config?.locked !== undefined ? config.locked : (existingFile.config?.locked || false),
|
|
89
|
+
mockVersion: existingFile.config?.mockVersion || 'source',
|
|
90
|
+
mockTime: config?.mock ? Date.now() : (existingFile.config?.mockTime || null),
|
|
91
|
+
}
|
|
92
|
+
await groupManager.setGroupFileConfig(currentGroupId, filename, configToSet)
|
|
93
|
+
|
|
94
|
+
ctx.body = createSuccessResponse({
|
|
95
|
+
success: true,
|
|
96
|
+
skipped: true,
|
|
97
|
+
file: {
|
|
98
|
+
id: existingFileEntry.id,
|
|
99
|
+
url: filename,
|
|
100
|
+
method: existingFile.method || method,
|
|
101
|
+
status: existingFile.status || status,
|
|
102
|
+
date: existingFile.date || Date.now(),
|
|
103
|
+
...configToSet,
|
|
104
|
+
rule: existingFile.rule || session.req?.rule || '',
|
|
105
|
+
pattern: existingFile.pattern || session.req?.pattern || '',
|
|
106
|
+
ruleValue: existingFile.ruleValue || 'pathname',
|
|
107
|
+
name: filename,
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 物理文件不存在(索引-物理文件不一致):删除旧记录,重新创建
|
|
114
|
+
console.warn(`${LOG_PREFIX.FILE_SAVE} 索引-物理文件不一致,删除旧记录并重新创建: ${filename}`)
|
|
115
|
+
await v3Storage.updateIndex(currentGroupId, {
|
|
116
|
+
...indexData,
|
|
117
|
+
files: indexData.files.filter(f => f.url !== filename),
|
|
118
|
+
})
|
|
84
119
|
}
|
|
85
120
|
|
|
86
121
|
// 生成新的文件ID
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件名: match-version-router.js
|
|
3
|
+
* 功能: 条件匹配规则路由(match_versions CRUD + 模式切换)
|
|
4
|
+
*
|
|
5
|
+
* 接口列表:
|
|
6
|
+
* - POST /cgi-bin/mockbubu/get-match-versions 获取匹配规则列表
|
|
7
|
+
* - POST /cgi-bin/mockbubu/add-match-version 新增匹配规则
|
|
8
|
+
* - POST /cgi-bin/mockbubu/update-match-version 更新规则条件和内容
|
|
9
|
+
* - POST /cgi-bin/mockbubu/update-match-version-meta 更新规则元信息(名称/描述)
|
|
10
|
+
* - POST /cgi-bin/mockbubu/delete-match-version 删除匹配规则
|
|
11
|
+
* - POST /cgi-bin/mockbubu/set-match-mode 切换匹配模式
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { createSuccessResponse } = require('../../utils/util')
|
|
15
|
+
const { wrapRouteHandler, validateRequiredParams, getFileIdByUrl } = require('../../utils/router-helpers')
|
|
16
|
+
const { MATCH_MODE_VALUE } = require('../../../config/const')
|
|
17
|
+
|
|
18
|
+
module.exports = (router) => {
|
|
19
|
+
// 获取匹配规则列表
|
|
20
|
+
router.post('/cgi-bin/mockbubu/get-match-versions', wrapRouteHandler(async (ctx) => {
|
|
21
|
+
const { groupManager } = ctx
|
|
22
|
+
const { url } = ctx.request.body || {}
|
|
23
|
+
|
|
24
|
+
validateRequiredParams({ url }, ['url'])
|
|
25
|
+
|
|
26
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
27
|
+
const matchVersions = await groupManager.getMatchVersions(currentGroupId, url)
|
|
28
|
+
|
|
29
|
+
ctx.body = createSuccessResponse({ list: matchVersions }, '获取匹配规则列表成功')
|
|
30
|
+
}, '获取匹配规则列表'))
|
|
31
|
+
|
|
32
|
+
// 新增匹配规则
|
|
33
|
+
router.post('/cgi-bin/mockbubu/add-match-version', wrapRouteHandler(async (ctx) => {
|
|
34
|
+
const { groupManager } = ctx
|
|
35
|
+
const { url, name, description = '' } = ctx.request.body || {}
|
|
36
|
+
|
|
37
|
+
validateRequiredParams({ url, name }, ['url', 'name'])
|
|
38
|
+
|
|
39
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
40
|
+
|
|
41
|
+
const newMatchVersion = await groupManager.createMatchVersion(currentGroupId, url, {
|
|
42
|
+
name,
|
|
43
|
+
description,
|
|
44
|
+
conditions: [], // 新规则默认空条件(兜底匹配)
|
|
45
|
+
content: {},
|
|
46
|
+
priority: 0,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
ctx.body = createSuccessResponse(newMatchVersion, '匹配规则创建成功')
|
|
50
|
+
}, '新增匹配规则'))
|
|
51
|
+
|
|
52
|
+
// 更新规则(条件 + 内容)
|
|
53
|
+
router.post('/cgi-bin/mockbubu/update-match-version', wrapRouteHandler(async (ctx) => {
|
|
54
|
+
const { groupManager } = ctx
|
|
55
|
+
const { url, matchVersionId, conditions, content } = ctx.request.body || {}
|
|
56
|
+
|
|
57
|
+
validateRequiredParams({ url, matchVersionId }, ['url', 'matchVersionId'])
|
|
58
|
+
|
|
59
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
60
|
+
|
|
61
|
+
const updates = {}
|
|
62
|
+
if (conditions !== undefined) updates.conditions = conditions
|
|
63
|
+
if (content !== undefined) updates.content = content
|
|
64
|
+
|
|
65
|
+
await groupManager.updateMatchVersion(currentGroupId, url, matchVersionId, updates)
|
|
66
|
+
|
|
67
|
+
ctx.body = createSuccessResponse(null, '匹配规则更新成功')
|
|
68
|
+
}, '更新匹配规则'))
|
|
69
|
+
|
|
70
|
+
// 更新规则元信息(名称 / 描述)
|
|
71
|
+
router.post('/cgi-bin/mockbubu/update-match-version-meta', wrapRouteHandler(async (ctx) => {
|
|
72
|
+
const { groupManager } = ctx
|
|
73
|
+
const { url, matchVersionId, name, description } = ctx.request.body || {}
|
|
74
|
+
|
|
75
|
+
validateRequiredParams({ url, matchVersionId }, ['url', 'matchVersionId'])
|
|
76
|
+
|
|
77
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
78
|
+
|
|
79
|
+
const updates = {}
|
|
80
|
+
if (name !== undefined) updates.name = name
|
|
81
|
+
if (description !== undefined) updates.description = description
|
|
82
|
+
|
|
83
|
+
await groupManager.updateMatchVersion(currentGroupId, url, matchVersionId, updates)
|
|
84
|
+
|
|
85
|
+
ctx.body = createSuccessResponse(null, '规则信息更新成功')
|
|
86
|
+
}, '更新规则元信息'))
|
|
87
|
+
|
|
88
|
+
// 删除匹配规则
|
|
89
|
+
router.post('/cgi-bin/mockbubu/delete-match-version', wrapRouteHandler(async (ctx) => {
|
|
90
|
+
const { groupManager } = ctx
|
|
91
|
+
const { url, matchVersionId } = ctx.request.body || {}
|
|
92
|
+
|
|
93
|
+
validateRequiredParams({ url, matchVersionId }, ['url', 'matchVersionId'])
|
|
94
|
+
|
|
95
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
96
|
+
const fileConfig = await groupManager.getGroupFileConfig(currentGroupId, url)
|
|
97
|
+
|
|
98
|
+
await groupManager.deleteMatchVersion(currentGroupId, url, matchVersionId)
|
|
99
|
+
|
|
100
|
+
// 如果删除的是条件匹配模式中的最后一条规则,自动切回版本直选模式
|
|
101
|
+
const remaining = await groupManager.getMatchVersions(currentGroupId, url)
|
|
102
|
+
if (remaining.length === 0 && fileConfig.mockVersion === MATCH_MODE_VALUE) {
|
|
103
|
+
const restoredVersion = fileConfig.prevMockVersion || null
|
|
104
|
+
fileConfig.mockVersion = restoredVersion
|
|
105
|
+
fileConfig.prevMockVersion = null
|
|
106
|
+
await groupManager.setGroupFileConfig(currentGroupId, url, fileConfig)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
ctx.body = createSuccessResponse(null, '匹配规则删除成功')
|
|
110
|
+
}, '删除匹配规则'))
|
|
111
|
+
|
|
112
|
+
// 切换匹配模式(版本直选 ↔ 条件匹配)
|
|
113
|
+
router.post('/cgi-bin/mockbubu/set-match-mode', wrapRouteHandler(async (ctx) => {
|
|
114
|
+
const { storageAdapter, groupManager } = ctx
|
|
115
|
+
const { url, enable } = ctx.request.body || {}
|
|
116
|
+
|
|
117
|
+
validateRequiredParams({ url }, ['url'])
|
|
118
|
+
|
|
119
|
+
const currentGroupId = await groupManager.getCurrentGroupId()
|
|
120
|
+
const fileConfig = await groupManager.getGroupFileConfig(currentGroupId, url)
|
|
121
|
+
|
|
122
|
+
if (enable) {
|
|
123
|
+
// 启用条件匹配模式
|
|
124
|
+
const prevMockVersion = fileConfig.mockVersion !== MATCH_MODE_VALUE
|
|
125
|
+
? fileConfig.mockVersion
|
|
126
|
+
: fileConfig.prevMockVersion
|
|
127
|
+
|
|
128
|
+
fileConfig.prevMockVersion = prevMockVersion || null
|
|
129
|
+
fileConfig.mockVersion = MATCH_MODE_VALUE
|
|
130
|
+
|
|
131
|
+
await groupManager.setGroupFileConfig(currentGroupId, url, fileConfig)
|
|
132
|
+
|
|
133
|
+
// 如果 match_versions 为空,自动创建默认规则(内容复制 source body)
|
|
134
|
+
const matchVersions = await groupManager.getMatchVersions(currentGroupId, url)
|
|
135
|
+
if (matchVersions.length === 0) {
|
|
136
|
+
// 获取 source body 作为默认内容
|
|
137
|
+
let defaultContent = {}
|
|
138
|
+
try {
|
|
139
|
+
const { fileId } = await getFileIdByUrl(ctx, url)
|
|
140
|
+
const v3Storage = storageAdapter.v3Storage
|
|
141
|
+
const fileData = await v3Storage.getFile(currentGroupId, fileId)
|
|
142
|
+
if (fileData && fileData.session && fileData.session.res && fileData.session.res.body) {
|
|
143
|
+
const bodyData = fileData.session.res.body
|
|
144
|
+
defaultContent = typeof bodyData === 'string' ? JSON.parse(bodyData) : bodyData
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// 解析失败时使用空对象
|
|
148
|
+
defaultContent = {}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await groupManager.createMatchVersion(currentGroupId, url, {
|
|
152
|
+
name: '默认',
|
|
153
|
+
description: '兜底规则(空条件匹配所有请求)',
|
|
154
|
+
conditions: [],
|
|
155
|
+
content: defaultContent,
|
|
156
|
+
priority: 0,
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const freshMatchVersions = await groupManager.getMatchVersions(currentGroupId, url)
|
|
161
|
+
ctx.body = createSuccessResponse({
|
|
162
|
+
prevMockVersion,
|
|
163
|
+
matchVersions: freshMatchVersions,
|
|
164
|
+
}, '已切换为条件匹配模式')
|
|
165
|
+
} else {
|
|
166
|
+
// 禁用条件匹配模式,恢复上次版本
|
|
167
|
+
const restoredVersion = fileConfig.prevMockVersion || null
|
|
168
|
+
fileConfig.mockVersion = restoredVersion
|
|
169
|
+
fileConfig.prevMockVersion = null
|
|
170
|
+
|
|
171
|
+
await groupManager.setGroupFileConfig(currentGroupId, url, fileConfig)
|
|
172
|
+
|
|
173
|
+
ctx.body = createSuccessResponse({
|
|
174
|
+
restoredMockVersion: restoredVersion,
|
|
175
|
+
}, '已切换为版本直选模式')
|
|
176
|
+
}
|
|
177
|
+
}, '切换匹配模式'))
|
|
178
|
+
}
|
|
@@ -11,6 +11,7 @@ const fileCrudRouter = require('./files/file-crud-router')
|
|
|
11
11
|
const fileConfigRouter = require('./files/file-config-router')
|
|
12
12
|
const versionRouter = require('./files/version-router')
|
|
13
13
|
const batchOperationsRouter = require('./files/batch-operations-router')
|
|
14
|
+
const matchVersionRouter = require('./files/match-version-router')
|
|
14
15
|
|
|
15
16
|
// groups/ - 组管理路由
|
|
16
17
|
const groupRouter = require('./groups/group-router')
|
|
@@ -30,6 +31,7 @@ module.exports = (router, options) => {
|
|
|
30
31
|
fileConfigRouter(router)
|
|
31
32
|
versionRouter(router)
|
|
32
33
|
batchOperationsRouter(router)
|
|
34
|
+
matchVersionRouter(router)
|
|
33
35
|
|
|
34
36
|
// groups/ - 注册组管理路由
|
|
35
37
|
groupRouter(router)
|
|
@@ -107,7 +107,7 @@ const filterFileConfig = (fileConfig) => {
|
|
|
107
107
|
return filtered
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
//
|
|
110
|
+
// 获取文件列表(轻量元数据,不含 session)
|
|
111
111
|
// 完全隔离架构:直接获取当前组的文件列表
|
|
112
112
|
const getFullDataList = async (storageAdapter, groupManager, groupId) => {
|
|
113
113
|
// 获取组的物理文件列表并合并配置
|
|
@@ -121,7 +121,9 @@ const getFullDataList = async (storageAdapter, groupManager, groupId) => {
|
|
|
121
121
|
// 获取索引以查找真实的 fileId(只读取一次)
|
|
122
122
|
const fileIndexData = await v3Storage.getIndex(groupId)
|
|
123
123
|
|
|
124
|
-
// ✅
|
|
124
|
+
// ✅ 读取文件元数据和配置(不含 session,按需通过 get-api-data 加载)
|
|
125
|
+
const brokenUrls = [] // 收集物理文件不存在的坏记录,最后批量从索引删除
|
|
126
|
+
|
|
125
127
|
for (let idx = 0; idx < list.length; idx++) {
|
|
126
128
|
const item = list[idx]
|
|
127
129
|
|
|
@@ -134,7 +136,14 @@ const getFullDataList = async (storageAdapter, groupManager, groupId) => {
|
|
|
134
136
|
// 读取 file.json (包含 session 数据和配置)
|
|
135
137
|
const fileData = await v3Storage.getFile(groupId, fileId)
|
|
136
138
|
|
|
137
|
-
//
|
|
139
|
+
// 物理文件丢失或损坏:收集后批量清理,避免单条异常导致整个列表接口崩溃
|
|
140
|
+
if (!fileData) {
|
|
141
|
+
console.warn(`[util] 物理文件不存在,标记清理: groupId=${groupId}, fileId=${fileId}`)
|
|
142
|
+
brokenUrls.push(item.name)
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 返回轻量元数据(config 配置 + 索引信息)
|
|
138
147
|
result.push({
|
|
139
148
|
name: item.name,
|
|
140
149
|
id: fileId,
|
|
@@ -150,17 +159,28 @@ const getFullDataList = async (storageAdapter, groupManager, groupId) => {
|
|
|
150
159
|
rule: indexEntry.rule || '',
|
|
151
160
|
ruleValue: indexEntry.ruleValue || 'pathname',
|
|
152
161
|
pattern: indexEntry.pattern || '',
|
|
162
|
+
remark: fileData.remark || '',
|
|
153
163
|
domain: indexEntry.domain || null,
|
|
154
164
|
pathname: indexEntry.pathname || null,
|
|
155
165
|
urlHash: indexEntry.urlHash || null,
|
|
156
166
|
// ✅ 添加 captureTime 字段(与 api-list 保持一致)
|
|
157
167
|
captureTime: indexEntry.createTime,
|
|
158
|
-
// ✅ 直接从 file.json 读取 session 数据
|
|
159
|
-
session: fileData.session || null,
|
|
160
168
|
})
|
|
161
169
|
}
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
// 批量清理索引中物理文件不存在的坏记录(一次写入)
|
|
173
|
+
if (brokenUrls.length > 0) {
|
|
174
|
+
console.warn(`[util] 批量清理索引坏记录: ${brokenUrls.length} 条`, brokenUrls)
|
|
175
|
+
const cleanedIndex = {
|
|
176
|
+
...fileIndexData,
|
|
177
|
+
files: fileIndexData.files.filter(f => !brokenUrls.includes(f.url)),
|
|
178
|
+
}
|
|
179
|
+
v3Storage.updateIndex(groupId, cleanedIndex).catch(err => {
|
|
180
|
+
console.error('[util] 清理索引坏记录失败:', err.message)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
164
184
|
return result
|
|
165
185
|
}
|
|
166
186
|
|