whistle.mockbubu 2.2.0-beta.1 → 2.2.0-beta.3

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.
@@ -78,6 +78,12 @@ async function captureResponse(params) {
78
78
  return
79
79
  }
80
80
 
81
+ const resContentType = svrRes.headers['content-type'] || ''
82
+ if (!resContentType.includes('application/json')) {
83
+ console.log(`[mockbubu] ⏭️ 非 JSON 响应,跳过捕获: ${filename} (Content-Type: ${resContentType})`)
84
+ return
85
+ }
86
+
81
87
  console.log(`[mockbubu 2025/12/8 ${new Date().toLocaleTimeString()}] 📦 响应接收完成,开始解压: ${filename}`)
82
88
  // 解压响应体
83
89
  const content = await handleBuffer2String({ body, encoding })
@@ -124,7 +124,6 @@ async function matchByConditions(params) {
124
124
 
125
125
  const allMatch = rule.conditions.every((cond) => {
126
126
  const sourceParams = cond.source === 'query' ? queryParams : payloadParams
127
- // 精确匹配:参数名和参数值必须完全相等(都转为字符串比较)
128
127
  return String(sourceParams[cond.key]) === String(cond.value)
129
128
  })
130
129
 
@@ -182,11 +181,9 @@ function extractQueryParams(originalReq) {
182
181
  }
183
182
 
184
183
  /**
185
- * 从原始请求中提取 payload 参数(仅支持 JSON,只提取第一层 key-value)
184
+ * 从原始请求中提取 payload 参数(支持 JSON 和 x-www-form-urlencoded,只提取第一层 key-value)
186
185
  * 来源:req.originalReq.body
187
186
  *
188
- * 非 JSON payload 时返回空对象,条件匹配只会比较 query 部分。
189
- *
190
187
  * @param {Object} originalReq - Whistle 原始请求对象
191
188
  * @returns {Object} payload 参数 key-value 对(值统一转为字符串)
192
189
  */
@@ -194,14 +191,15 @@ function extractPayloadParams(originalReq) {
194
191
  if (!originalReq) return {}
195
192
 
196
193
  try {
197
- let body = originalReq.body || ''
194
+ // 优先使用 mock 路径预缓冲的请求体,fallback Whistle 预解析的 body
195
+ let body = originalReq._reqBody || originalReq.body || ''
198
196
 
199
197
  // Buffer 转字符串
200
198
  if (Buffer.isBuffer(body)) {
201
199
  body = body.toString('utf8')
202
200
  }
203
201
 
204
- // 修复3:body 可能已被 Whistle 预解析为对象,直接提取而非走 JSON.parse
202
+ // body 可能已被 Whistle 预解析为对象,直接提取
205
203
  if (typeof body === 'object' && body !== null) {
206
204
  return extractFlatParams(body)
207
205
  }
@@ -210,12 +208,26 @@ function extractPayloadParams(originalReq) {
210
208
  return {}
211
209
  }
212
210
 
213
- const parsed = JSON.parse(body)
214
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
215
- return {}
211
+ // 尝试 JSON 格式
212
+ try {
213
+ const parsed = JSON.parse(body)
214
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
215
+ return extractFlatParams(parsed)
216
+ }
217
+ } catch {
218
+ // 不是 JSON,继续尝试
216
219
  }
217
220
 
218
- return extractFlatParams(parsed)
221
+ // 尝试 application/x-www-form-urlencoded 格式
222
+ if (body.includes('=')) {
223
+ const result = {}
224
+ new URLSearchParams(body).forEach((value, key) => {
225
+ result[key] = value
226
+ })
227
+ if (Object.keys(result).length > 0) return result
228
+ }
229
+
230
+ return {}
219
231
  } catch {
220
232
  return {}
221
233
  }
@@ -23,6 +23,19 @@ const {
23
23
  } = require('./capture-handler')
24
24
  const pluginModeManager = require('../plugin-mode-manager')
25
25
 
26
+ /**
27
+ * 从 req 流缓冲请求体,供条件匹配使用
28
+ * 仅在 mock 路径调用,此时 req 流未被 pipe,可安全读取
29
+ */
30
+ function bufferReqBody(req) {
31
+ return new Promise((resolve) => {
32
+ const chunks = []
33
+ req.on('data', chunk => chunks.push(chunk))
34
+ req.on('end', () => resolve(Buffer.concat(chunks)))
35
+ req.on('error', () => resolve(Buffer.alloc(0)))
36
+ })
37
+ }
38
+
26
39
  module.exports = (server, options) => {
27
40
  console.log('[mockbubu] 🚀 server.js 模块已加载!')
28
41
  const { storage } = options
@@ -101,6 +114,7 @@ module.exports = (server, options) => {
101
114
 
102
115
  // 有 mock 且有数据 → 返回 mock(不捕获)
103
116
  console.log(`[mockbubu] ✅ Mock-Only 模式: 返回 Mock 数据(不捕获) | URL: ${url}`)
117
+ originalReq._reqBody = await bufferReqBody(req)
104
118
  const mockOnlyResult = await sendMockResponse(res, sourceData, {
105
119
  mockVersion,
106
120
  groupManager,
@@ -117,6 +131,7 @@ module.exports = (server, options) => {
117
131
 
118
132
  // 场景 1: Mock 启用 + 有源数据 → 返回 Mock 数据
119
133
  if (mock && sourceData) {
134
+ originalReq._reqBody = await bufferReqBody(req)
120
135
  const mockResult = await sendMockResponse(res, sourceData, {
121
136
  mockVersion,
122
137
  groupManager,
@@ -370,6 +370,7 @@ const getRule = (originalReq) => {
370
370
  const isJsonReq = (headers) => {
371
371
  const result = (
372
372
  (headers.accept && headers.accept.includes('application/json')) ||
373
+ headers.accept === '*/*' ||
373
374
  headers['sec-fetch-dest'] === 'empty' ||
374
375
  headers['Sec-Fetch-Dest'] === 'empty'
375
376
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whistle.mockbubu",
3
- "version": "2.2.0-beta.1",
3
+ "version": "2.2.0-beta.3",
4
4
  "description": "mock response data",
5
5
  "scripts": {
6
6
  "lint": "eslint . --ext .js",
package/public/js/app.js CHANGED
@@ -243,6 +243,36 @@ const __default__ = {
243
243
 
244
244
  // 是否已加载已保存文件
245
245
  let savedFilesLoaded = false;
246
+
247
+ /**
248
+ * 从 session 中提取 URL 查询参数
249
+ *
250
+ * x-whistle-full-url 是 Whistle 内部 header,只在 originalReq 阶段可靠。
251
+ * getSession() 返回的 session.req.headers 可能已被剥离该字段,
252
+ * 因此先尝试 x-whistle-full-url,不存在时 fallback 到 session.url(Whistle session 顶层字段,含完整 query string)。
253
+ */
254
+ const extractQueryFromSession = session => {
255
+ const fullUrlEncoded = session?.req?.headers?.['x-whistle-full-url'];
256
+ if (fullUrlEncoded) {
257
+ try {
258
+ const urlObj = new URL(decodeURIComponent(fullUrlEncoded));
259
+ return Object.fromEntries(urlObj.searchParams);
260
+ } catch (e) {
261
+ // 解析失败,继续 fallback
262
+ }
263
+ }
264
+ // Whistle session 顶层 url 字段包含完整 URL(含 query string)
265
+ const reqUrl = session?.url;
266
+ if (reqUrl) {
267
+ try {
268
+ const urlObj = new URL(reqUrl);
269
+ return Object.fromEntries(urlObj.searchParams);
270
+ } catch (e) {
271
+ // 解析失败,返回空对象
272
+ }
273
+ }
274
+ return {};
275
+ };
246
276
  const getRules = rules => {
247
277
  const {
248
278
  defaultRules,
@@ -384,17 +414,7 @@ const __default__ = {
384
414
  // 映射未保存数据的展示字段
385
415
  const newCachedItems = filteredData.map(item => {
386
416
  // 从 session 中提取 URL 查询参数
387
- let query = {};
388
- const fullUrlEncoded = item.session?.req?.headers?.['x-whistle-full-url'];
389
- if (fullUrlEncoded) {
390
- try {
391
- const fullUrl = decodeURIComponent(fullUrlEncoded);
392
- const urlObj = new URL(fullUrl);
393
- query = Object.fromEntries(urlObj.searchParams);
394
- } catch (e) {
395
- // 静默处理URL解析错误
396
- }
397
- }
417
+ const query = extractQueryFromSession(item.session);
398
418
 
399
419
  // 从 session 中提取 payload (请求体)
400
420
  const payload = item.session?.req?.body || null;
@@ -515,17 +535,7 @@ const __default__ = {
515
535
  }
516
536
 
517
537
  // 从 session 提取 query 参数
518
- let query = {};
519
- const fullUrlEncoded = data.session?.req?.headers?.['x-whistle-full-url'];
520
- if (fullUrlEncoded) {
521
- try {
522
- const fullUrl = decodeURIComponent(fullUrlEncoded);
523
- const urlObj = new URL(fullUrl);
524
- query = Object.fromEntries(urlObj.searchParams);
525
- } catch (e) {
526
- // 静默处理URL解析错误
527
- }
528
- }
538
+ const query = extractQueryFromSession(data.session);
529
539
 
530
540
  // 使用 Object.assign 更新 currentRow,保留引用
531
541
  Object.assign(currentRow.value, {
@@ -1271,12 +1281,13 @@ const __default__ = {
1271
1281
  }
1272
1282
  };
1273
1283
  (0,vue__WEBPACK_IMPORTED_MODULE_21__.watch)(currentRow, val => {
1274
- // 只对已保存的文件调用 getCurrentRowData
1275
- // 未保存的缓存数据已经包含完整 session,不需要再次获取
1276
- // 判断依据: 有 id 字段说明是已保存的文件,需要按需加载完整数据
1277
1284
  if (val && val.id) {
1278
- // id 字段说明是已保存的文件,需要按需加载完整数据
1285
+ // 已保存文件:按需加载完整数据(含 query/payload/headers)
1279
1286
  getCurrentRowData(val);
1287
+ } else if (val && val.session) {
1288
+ // 缓存数据:session 已在轮询时附带,但 query 可能在旧版本代码下提取失败
1289
+ // 切换行时重新提取,确保展示正确
1290
+ val.query = extractQueryFromSession(val.session);
1280
1291
  }
1281
1292
  });
1282
1293
  (0,vue__WEBPACK_IMPORTED_MODULE_21__.onMounted)(async () => {
@@ -1335,6 +1346,7 @@ const __default__ = {
1335
1346
  pollingStartTime,
1336
1347
  intervalId,
1337
1348
  savedFilesLoaded,
1349
+ extractQueryFromSession,
1338
1350
  getRules,
1339
1351
  getInit,
1340
1352
  pageShow,
@@ -2173,10 +2185,19 @@ __webpack_require__.r(__webpack_exports__);
2173
2185
  /* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_7__);
2174
2186
  /* harmony import */ var core_js_modules_es_iterator_some_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! core-js/modules/es.iterator.some.js */ "./node_modules/core-js/modules/es.iterator.some.js");
2175
2187
  /* harmony import */ var core_js_modules_es_iterator_some_js__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_some_js__WEBPACK_IMPORTED_MODULE_8__);
2176
- /* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! vue */ "./node_modules/vue/dist/vue.runtime.esm.js");
2177
- /* harmony import */ var _common_JsonEditor_vue__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ../common/JsonEditor.vue */ "./src/components/common/JsonEditor.vue");
2178
- /* harmony import */ var _MatchConditionEditor_vue__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./MatchConditionEditor.vue */ "./src/components/content/MatchConditionEditor.vue");
2179
- /* harmony import */ var _service__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ../../service */ "./src/service/index.js");
2188
+ /* harmony import */ var core_js_modules_web_url_search_params_delete_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! core-js/modules/web.url-search-params.delete.js */ "./node_modules/core-js/modules/web.url-search-params.delete.js");
2189
+ /* harmony import */ var core_js_modules_web_url_search_params_delete_js__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_web_url_search_params_delete_js__WEBPACK_IMPORTED_MODULE_9__);
2190
+ /* harmony import */ var core_js_modules_web_url_search_params_has_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! core-js/modules/web.url-search-params.has.js */ "./node_modules/core-js/modules/web.url-search-params.has.js");
2191
+ /* harmony import */ var core_js_modules_web_url_search_params_has_js__WEBPACK_IMPORTED_MODULE_10___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_web_url_search_params_has_js__WEBPACK_IMPORTED_MODULE_10__);
2192
+ /* harmony import */ var core_js_modules_web_url_search_params_size_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! core-js/modules/web.url-search-params.size.js */ "./node_modules/core-js/modules/web.url-search-params.size.js");
2193
+ /* harmony import */ var core_js_modules_web_url_search_params_size_js__WEBPACK_IMPORTED_MODULE_11___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_web_url_search_params_size_js__WEBPACK_IMPORTED_MODULE_11__);
2194
+ /* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! vue */ "./node_modules/vue/dist/vue.runtime.esm.js");
2195
+ /* harmony import */ var _common_JsonEditor_vue__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ../common/JsonEditor.vue */ "./src/components/common/JsonEditor.vue");
2196
+ /* harmony import */ var _MatchConditionEditor_vue__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! ./MatchConditionEditor.vue */ "./src/components/content/MatchConditionEditor.vue");
2197
+ /* harmony import */ var _service__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! ../../service */ "./src/service/index.js");
2198
+
2199
+
2200
+
2180
2201
 
2181
2202
 
2182
2203
 
@@ -2214,17 +2235,17 @@ const __default__ = {
2214
2235
  }) {
2215
2236
  const props = __props;
2216
2237
  const MATCH_MODE_VALUE = '__match_mode__';
2217
- const editRef = (0,vue__WEBPACK_IMPORTED_MODULE_9__.ref)(null);
2218
- const fullScreenEditorRef = (0,vue__WEBPACK_IMPORTED_MODULE_9__.ref)(null);
2219
- const versionNameInput = (0,vue__WEBPACK_IMPORTED_MODULE_9__.ref)(null);
2220
- const matchRuleNameInput = (0,vue__WEBPACK_IMPORTED_MODULE_9__.ref)(null);
2221
- const responseList = (0,vue__WEBPACK_IMPORTED_MODULE_9__.ref)([]);
2222
- const currentFile = (0,vue__WEBPACK_IMPORTED_MODULE_9__.ref)({
2238
+ const editRef = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(null);
2239
+ const fullScreenEditorRef = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(null);
2240
+ const versionNameInput = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(null);
2241
+ const matchRuleNameInput = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(null);
2242
+ const responseList = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)([]);
2243
+ const currentFile = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)({
2223
2244
  content: {}
2224
2245
  });
2225
- const hasJsonError = (0,vue__WEBPACK_IMPORTED_MODULE_9__.ref)(false); // JSON 格式错误标志
2226
- const hasFullScreenJsonError = (0,vue__WEBPACK_IMPORTED_MODULE_9__.ref)(false); // 全屏编辑器 JSON 格式错误标志
2227
- const versionModal = (0,vue__WEBPACK_IMPORTED_MODULE_9__.reactive)({
2246
+ const hasJsonError = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(false); // JSON 格式错误标志
2247
+ const hasFullScreenJsonError = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(false); // 全屏编辑器 JSON 格式错误标志
2248
+ const versionModal = (0,vue__WEBPACK_IMPORTED_MODULE_12__.reactive)({
2228
2249
  visible: false,
2229
2250
  status: 'create',
2230
2251
  // 'create' | 'edit'
@@ -2232,24 +2253,24 @@ const __default__ = {
2232
2253
  description: '',
2233
2254
  editingVersion: null // 正在编辑的版本对象
2234
2255
  });
2235
- const fullScreenModal = (0,vue__WEBPACK_IMPORTED_MODULE_9__.reactive)({
2256
+ const fullScreenModal = (0,vue__WEBPACK_IMPORTED_MODULE_12__.reactive)({
2236
2257
  visible: false,
2237
2258
  content: {}
2238
2259
  });
2239
2260
 
2240
2261
  // ========== 条件匹配模式状态 ==========
2241
2262
  // 当前显示模式:'version' = 版本直选,'match' = 条件匹配
2242
- const mockMode = (0,vue__WEBPACK_IMPORTED_MODULE_9__.ref)('version');
2263
+ const mockMode = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)('version');
2243
2264
  // 模式下拉是否展开
2244
- const modeDropdownVisible = (0,vue__WEBPACK_IMPORTED_MODULE_9__.ref)(false);
2265
+ const modeDropdownVisible = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(false);
2245
2266
  // 匹配规则列表
2246
- const matchRuleList = (0,vue__WEBPACK_IMPORTED_MODULE_9__.ref)([]);
2267
+ const matchRuleList = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)([]);
2247
2268
  // 当前选中的匹配规则(深拷贝,供编辑用)
2248
- const currentMatchRule = (0,vue__WEBPACK_IMPORTED_MODULE_9__.ref)(null);
2269
+ const currentMatchRule = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(null);
2249
2270
  // 条件或内容是否有变更(控制保存按钮显示)
2250
- const matchRuleChanged = (0,vue__WEBPACK_IMPORTED_MODULE_9__.ref)(false);
2271
+ const matchRuleChanged = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(false);
2251
2272
  // 匹配规则创建/编辑弹窗
2252
- const matchRuleModal = (0,vue__WEBPACK_IMPORTED_MODULE_9__.reactive)({
2273
+ const matchRuleModal = (0,vue__WEBPACK_IMPORTED_MODULE_12__.reactive)({
2253
2274
  visible: false,
2254
2275
  status: 'create',
2255
2276
  // 'create' | 'edit'
@@ -2258,19 +2279,63 @@ const __default__ = {
2258
2279
  editingRule: null
2259
2280
  });
2260
2281
  // 捕获到的参数(来自 file.json session,用于"从捕获参数选择")
2261
- const capturedQuery = (0,vue__WEBPACK_IMPORTED_MODULE_9__.computed)(() => props.api.query || {});
2262
- const capturedPayload = (0,vue__WEBPACK_IMPORTED_MODULE_9__.computed)(() => {
2282
+ const capturedQuery = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => props.api.query || {});
2283
+ const capturedPayload = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => {
2263
2284
  const payload = props.api.payload;
2264
2285
  if (!payload) return {};
2265
- // payload 结构: { raw, parsed, type },取 parsed.data
2266
- return payload?.parsed?.data || {};
2286
+ // 结构化对象格式: { raw, parsed: { type, data } }
2287
+ if (typeof payload === 'object' && payload.parsed?.data) {
2288
+ return payload.parsed.data;
2289
+ }
2290
+ // 普通对象(Whistle 预解析),提取第一层 string/number/boolean
2291
+ if (typeof payload === 'object' && !Array.isArray(payload)) {
2292
+ const result = {};
2293
+ Object.keys(payload).forEach(key => {
2294
+ const val = payload[key];
2295
+ if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
2296
+ result[key] = String(val);
2297
+ }
2298
+ });
2299
+ return result;
2300
+ }
2301
+ if (typeof payload === 'string') {
2302
+ // JSON 格式
2303
+ try {
2304
+ const parsed = JSON.parse(payload);
2305
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
2306
+ const result = {};
2307
+ Object.keys(parsed).forEach(key => {
2308
+ const val = parsed[key];
2309
+ if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
2310
+ result[key] = String(val);
2311
+ }
2312
+ });
2313
+ return result;
2314
+ }
2315
+ } catch {
2316
+ // 不是 JSON,继续尝试
2317
+ }
2318
+ // application/x-www-form-urlencoded 格式
2319
+ if (payload.includes('=')) {
2320
+ try {
2321
+ const result = {};
2322
+ new URLSearchParams(payload).forEach((value, key) => {
2323
+ result[key] = value;
2324
+ });
2325
+ if (Object.keys(result).length > 0) return result;
2326
+ } catch {
2327
+ // 解析失败
2328
+ }
2329
+ }
2330
+ }
2331
+ return {};
2267
2332
  });
2268
- const name = (0,vue__WEBPACK_IMPORTED_MODULE_9__.computed)(() => props.api.name);
2269
- const mock = (0,vue__WEBPACK_IMPORTED_MODULE_9__.computed)(() => props.api.mock);
2270
- const mockVersion = (0,vue__WEBPACK_IMPORTED_MODULE_9__.computed)(() => props.api.mockVersion);
2271
- const date = (0,vue__WEBPACK_IMPORTED_MODULE_9__.computed)(() => props.api.date);
2272
- const isSourceReadonly = (0,vue__WEBPACK_IMPORTED_MODULE_9__.computed)(() => mockMode.value === 'version' && currentFile.value.type === 'source');
2273
- const activeEditorContent = (0,vue__WEBPACK_IMPORTED_MODULE_9__.computed)(() => mockMode.value === 'match' ? currentMatchRule.value?.content : currentFile.value.content);
2333
+ const name = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => props.api.name);
2334
+ const mock = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => props.api.mock);
2335
+ const mockVersion = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => props.api.mockVersion);
2336
+ const date = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => props.api.date);
2337
+ const isSourceReadonly = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => mockMode.value === 'version' && currentFile.value.type === 'source');
2338
+ const activeEditorContent = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => mockMode.value === 'match' ? currentMatchRule.value?.content : currentFile.value.content);
2274
2339
 
2275
2340
  /**
2276
2341
  * 获取 source 版本数据(仅用于未保存文件)
@@ -2335,7 +2400,7 @@ const __default__ = {
2335
2400
  */
2336
2401
  const getHistoryVersions = () => {
2337
2402
  const fileId = props.api.id;
2338
- return (0,_service__WEBPACK_IMPORTED_MODULE_12__.getVersions)({
2403
+ return (0,_service__WEBPACK_IMPORTED_MODULE_15__.getVersions)({
2339
2404
  fileId
2340
2405
  }).then(res => {
2341
2406
  console.log('[ResponsePanel] 后端返回的版本数据:', res);
@@ -2414,7 +2479,7 @@ const __default__ = {
2414
2479
  }
2415
2480
  };
2416
2481
  const deleteVersionHandler = (item, index) => {
2417
- (0,_service__WEBPACK_IMPORTED_MODULE_12__.deleteVersion)({
2482
+ (0,_service__WEBPACK_IMPORTED_MODULE_15__.deleteVersion)({
2418
2483
  url: props.api.name,
2419
2484
  fileId: props.api.id,
2420
2485
  versionId: item.id
@@ -2489,7 +2554,7 @@ const __default__ = {
2489
2554
  // 未保存文件:先保存文件,再创建版本
2490
2555
  console.log('[ResponsePanel] 文件未保存,先保存文件');
2491
2556
  try {
2492
- const saveResult = await (0,_service__WEBPACK_IMPORTED_MODULE_12__.saveFile)({
2557
+ const saveResult = await (0,_service__WEBPACK_IMPORTED_MODULE_15__.saveFile)({
2493
2558
  url: props.api.url,
2494
2559
  method: props.api.method,
2495
2560
  status: props.api.status,
@@ -2520,7 +2585,7 @@ const __default__ = {
2520
2585
  return;
2521
2586
  }
2522
2587
  }
2523
- (0,_service__WEBPACK_IMPORTED_MODULE_12__.addVersion)({
2588
+ (0,_service__WEBPACK_IMPORTED_MODULE_15__.addVersion)({
2524
2589
  versionName: versionModal.name,
2525
2590
  fileId,
2526
2591
  description: versionModal.description,
@@ -2550,7 +2615,7 @@ const __default__ = {
2550
2615
  // 添加版本成功后,重新获取版本列表(确保数据同步)
2551
2616
  // ⚠️ 关键修复:使用保存的 fileId,而不是 props.api.id(可能仍是 undefined)
2552
2617
  console.log('[ResponsePanel] 使用 fileId 刷新版本列表:', fileId);
2553
- (0,_service__WEBPACK_IMPORTED_MODULE_12__.getVersions)({
2618
+ (0,_service__WEBPACK_IMPORTED_MODULE_15__.getVersions)({
2554
2619
  fileId
2555
2620
  }).then(versionRes => {
2556
2621
  console.log('[ResponsePanel] 后端返回的版本数据:', versionRes);
@@ -2621,7 +2686,7 @@ const __default__ = {
2621
2686
  }
2622
2687
 
2623
2688
  // 调用后端API更新版本元信息
2624
- (0,_service__WEBPACK_IMPORTED_MODULE_12__.updateVersionMeta)({
2689
+ (0,_service__WEBPACK_IMPORTED_MODULE_15__.updateVersionMeta)({
2625
2690
  url: props.api.name,
2626
2691
  fileId: props.api.id,
2627
2692
  versionId: currentFile.value.id,
@@ -2691,7 +2756,7 @@ const __default__ = {
2691
2756
  effect: tempEffect
2692
2757
  };
2693
2758
  const versionName = item.type === 'history' ? item.filename : '';
2694
- (0,_service__WEBPACK_IMPORTED_MODULE_12__.setMockVersion)({
2759
+ (0,_service__WEBPACK_IMPORTED_MODULE_15__.setMockVersion)({
2695
2760
  url: props.api.name,
2696
2761
  versionName
2697
2762
  }) // Changed from name to url
@@ -2713,7 +2778,7 @@ const __default__ = {
2713
2778
  currentFile.value.effect = true;
2714
2779
  };
2715
2780
  const updateHistory = () => {
2716
- (0,_service__WEBPACK_IMPORTED_MODULE_12__.updateVersionContent)({
2781
+ (0,_service__WEBPACK_IMPORTED_MODULE_15__.updateVersionContent)({
2717
2782
  versionId: currentFile.value.id,
2718
2783
  content: currentFile.value.content,
2719
2784
  fileId: props.api.id
@@ -2757,7 +2822,7 @@ const __default__ = {
2757
2822
  responseList.value = source ? [source, ...versions] : versions;
2758
2823
  };
2759
2824
  const updateApiDataHandler = () => {
2760
- (0,_service__WEBPACK_IMPORTED_MODULE_12__.updateApiData)(props.api.name, currentFile.value.content).then(res => {
2825
+ (0,_service__WEBPACK_IMPORTED_MODULE_15__.updateApiData)(props.api.name, currentFile.value.content).then(res => {
2761
2826
  const {
2762
2827
  code
2763
2828
  } = res;
@@ -2818,7 +2883,7 @@ const __default__ = {
2818
2883
  /** 保存当前匹配规则(条件 + 内容) */
2819
2884
  const updateMatchRuleHandler = async () => {
2820
2885
  if (!currentMatchRule.value) return;
2821
- const res = await (0,_service__WEBPACK_IMPORTED_MODULE_12__.updateMatchVersion)({
2886
+ const res = await (0,_service__WEBPACK_IMPORTED_MODULE_15__.updateMatchVersion)({
2822
2887
  url: props.api.name,
2823
2888
  matchVersionId: currentMatchRule.value.id,
2824
2889
  conditions: currentMatchRule.value.conditions,
@@ -3023,13 +3088,36 @@ const __default__ = {
3023
3088
  const switchMode = async targetMode => {
3024
3089
  modeDropdownVisible.value = false;
3025
3090
  if (targetMode === mockMode.value) return;
3026
- if (!props.api.id) {
3027
- element_ui_lib_message__WEBPACK_IMPORTED_MODULE_2___default().warning('未保存的文件需要先保存后才能使用条件匹配');
3028
- return;
3029
- }
3030
3091
  if (targetMode === 'match') {
3092
+ // 未保存文件:先自动保存,再切换
3093
+ if (!props.api.id) {
3094
+ try {
3095
+ const saveResult = await (0,_service__WEBPACK_IMPORTED_MODULE_15__.saveFile)({
3096
+ url: props.api.url,
3097
+ method: props.api.method,
3098
+ status: props.api.status,
3099
+ session: props.api.session,
3100
+ config: {
3101
+ mock: props.api.mock || false,
3102
+ locked: props.api.locked || false
3103
+ }
3104
+ });
3105
+ if (saveResult.code !== 200) {
3106
+ element_ui_lib_message__WEBPACK_IMPORTED_MODULE_2___default().error(`保存文件失败: ${saveResult.msg || '未知错误'}`);
3107
+ return;
3108
+ }
3109
+ emit('fileSaved', {
3110
+ url: props.api.url,
3111
+ savedFile: saveResult.data.file
3112
+ });
3113
+ } catch (err) {
3114
+ element_ui_lib_message__WEBPACK_IMPORTED_MODULE_2___default().error(`保存文件失败: ${err.message || '未知错误'}`);
3115
+ return;
3116
+ }
3117
+ }
3118
+
3031
3119
  // 切换为条件匹配
3032
- const res = await (0,_service__WEBPACK_IMPORTED_MODULE_12__.setMatchMode)({
3120
+ const res = await (0,_service__WEBPACK_IMPORTED_MODULE_15__.setMatchMode)({
3033
3121
  url: props.api.name,
3034
3122
  enable: true
3035
3123
  });
@@ -3046,7 +3134,7 @@ const __default__ = {
3046
3134
  emit('changeMockVersion', MATCH_MODE_VALUE);
3047
3135
  } else {
3048
3136
  // 切换回版本直选
3049
- const res = await (0,_service__WEBPACK_IMPORTED_MODULE_12__.setMatchMode)({
3137
+ const res = await (0,_service__WEBPACK_IMPORTED_MODULE_15__.setMatchMode)({
3050
3138
  url: props.api.name,
3051
3139
  enable: false
3052
3140
  });
@@ -3070,7 +3158,7 @@ const __default__ = {
3070
3158
  /** 加载匹配规则列表 */
3071
3159
  const loadMatchVersions = async () => {
3072
3160
  if (!props.api.id || !props.api.name) return;
3073
- const res = await (0,_service__WEBPACK_IMPORTED_MODULE_12__.getMatchVersions)({
3161
+ const res = await (0,_service__WEBPACK_IMPORTED_MODULE_15__.getMatchVersions)({
3074
3162
  url: props.api.name
3075
3163
  });
3076
3164
  if (res.code !== 200) {
@@ -3150,7 +3238,7 @@ const __default__ = {
3150
3238
  return;
3151
3239
  }
3152
3240
  if (matchRuleModal.status === 'create') {
3153
- const res = await (0,_service__WEBPACK_IMPORTED_MODULE_12__.addMatchVersion)({
3241
+ const res = await (0,_service__WEBPACK_IMPORTED_MODULE_15__.addMatchVersion)({
3154
3242
  url: props.api.name,
3155
3243
  name: matchRuleModal.name.trim(),
3156
3244
  description: matchRuleModal.description
@@ -3169,7 +3257,7 @@ const __default__ = {
3169
3257
  element_ui_lib_message__WEBPACK_IMPORTED_MODULE_2___default().success('创建成功');
3170
3258
  } else {
3171
3259
  // 编辑元信息
3172
- const res = await (0,_service__WEBPACK_IMPORTED_MODULE_12__.updateMatchVersionMeta)({
3260
+ const res = await (0,_service__WEBPACK_IMPORTED_MODULE_15__.updateMatchVersionMeta)({
3173
3261
  url: props.api.name,
3174
3262
  matchVersionId: matchRuleModal.editingRule.id,
3175
3263
  name: matchRuleModal.name.trim(),
@@ -3203,7 +3291,7 @@ const __default__ = {
3203
3291
  } catch {
3204
3292
  return; // 取消
3205
3293
  }
3206
- const res = await (0,_service__WEBPACK_IMPORTED_MODULE_12__.deleteMatchVersion)({
3294
+ const res = await (0,_service__WEBPACK_IMPORTED_MODULE_15__.deleteMatchVersion)({
3207
3295
  url: props.api.name,
3208
3296
  matchVersionId: rule.id
3209
3297
  });
@@ -3238,7 +3326,7 @@ const __default__ = {
3238
3326
  * - 已保存文件: file-${id}
3239
3327
  * - 未保存文件: cache-${url}-${captureTime}
3240
3328
  */
3241
- (0,vue__WEBPACK_IMPORTED_MODULE_9__.watch)(() => props.rowKey, (newRowKey, oldRowKey) => {
3329
+ (0,vue__WEBPACK_IMPORTED_MODULE_12__.watch)(() => props.rowKey, (newRowKey, oldRowKey) => {
3242
3330
  // 判断是否为已保存文件(已保存文件有 id,未保存文件没有)
3243
3331
  const isSavedFile = !!props.api.id;
3244
3332
 
@@ -3384,8 +3472,8 @@ const __default__ = {
3384
3472
  handleMatchRuleConfirm,
3385
3473
  deleteMatchRuleHandler,
3386
3474
  refreshVersions,
3387
- JsonEditor: _common_JsonEditor_vue__WEBPACK_IMPORTED_MODULE_10__["default"],
3388
- MatchConditionEditor: _MatchConditionEditor_vue__WEBPACK_IMPORTED_MODULE_11__["default"]
3475
+ JsonEditor: _common_JsonEditor_vue__WEBPACK_IMPORTED_MODULE_13__["default"],
3476
+ MatchConditionEditor: _MatchConditionEditor_vue__WEBPACK_IMPORTED_MODULE_14__["default"]
3389
3477
  };
3390
3478
  }
3391
3479
  }));
@@ -5274,7 +5362,7 @@ var render = function render() {
5274
5362
  staticClass: "response-panel"
5275
5363
  }, [_c("div", {
5276
5364
  staticClass: "response-panel__header"
5277
- }, [_setup.props.api.id ? _c("div", {
5365
+ }, [_setup.props.api ? _c("div", {
5278
5366
  staticClass: "response-panel__mode-selector"
5279
5367
  }, [_c("div", {
5280
5368
  staticClass: "response-panel__mode-btn",