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.
@@ -49,8 +49,7 @@ module.exports = (router) => {
49
49
  * method: 'GET',
50
50
  * status: 200,
51
51
  * mock: true,
52
- * locked: false,
53
- * session: {...} // 完整会话数据
52
+ * locked: false
54
53
  * }
55
54
  * ],
56
55
  * msg: '操作成功'
@@ -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
- console.log(`${LOG_PREFIX.FILE_SAVE} 文件已存在,跳过保存: ${filename}`)
82
- ctx.body = createSuccessResponse({ success: true, skipped: true })
83
- return
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
- // 获取文件列表(包含完整 session 数据)
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
- // ✅ 读取完整数据(包含 session),支持 Payload/Headers 视图
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
- // 返回完整数据(包含 config 配置字段 + session 数据)
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whistle.mockbubu",
3
- "version": "2.1.5",
3
+ "version": "2.2.0-beta.2",
4
4
  "description": "mock response data",
5
5
  "scripts": {
6
6
  "lint": "eslint . --ext .js",