whistle.mock-plugins 1.2.2-beta.2 → 1.2.2-beta.23

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/models.js CHANGED
@@ -22,6 +22,15 @@
22
22
  * @property {string} name - 响应名称
23
23
  * @property {string} description - 响应描述
24
24
  * @property {string} content - 响应内容,使用 Mock.js 语法
25
+ * @property {Array<ParamMatcher>} paramMatchers - 响应级入参匹配规则,运行时按顺序匹配,第一个满足所有规则的响应被返回;无规则的响应作为默认兜底
26
+ */
27
+
28
+ /**
29
+ * 参数匹配规则数据模型
30
+ * @typedef {Object} ParamMatcher
31
+ * @property {string} paramPath - 参数路径,支持嵌套路径(如 data.user.id)
32
+ * @property {string} paramValue - 期望值
33
+ * @property {string} matchType - 匹配类型,可选值: 'exact'(精确匹配), 'contains'(包含), 'regex'(正则)
25
34
  */
26
35
 
27
36
  /**
@@ -53,7 +53,8 @@ const processInterfaceData = (data) => {
53
53
  id: resp.id,
54
54
  name: resp.name,
55
55
  description: resp.description || '',
56
- content: resp.content || '{}'
56
+ content: resp.content || '{}',
57
+ paramMatchers: Array.isArray(resp.paramMatchers) ? resp.paramMatchers : []
57
58
  };
58
59
 
59
60
  console.log(`处理后的响应 ${index}:`, processedResp);
@@ -86,7 +87,8 @@ const processInterfaceData = (data) => {
86
87
  id: Math.random().toString(36).substring(2, 15),
87
88
  name: '默认响应',
88
89
  description: '',
89
- content: result.responseContent
90
+ content: result.responseContent,
91
+ paramMatchers: []
90
92
  }];
91
93
  result.activeResponseId = result.responses[0].id;
92
94
  console.log('创建的默认响应:', result.responses[0]);
@@ -98,7 +100,8 @@ const processInterfaceData = (data) => {
98
100
  id: Math.random().toString(36).substring(2, 15),
99
101
  name: '默认响应',
100
102
  description: '',
101
- content: '{}'
103
+ content: '{}',
104
+ paramMatchers: []
102
105
  }];
103
106
  result.activeResponseId = result.responses[0].id;
104
107
  result.responseContent = '{}';
@@ -0,0 +1,51 @@
1
+ const express = require('express');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const storage = require('../storage');
5
+
6
+ const DATA_DIR = storage.DATA_DIR;
7
+ const SETTINGS_FILE = path.join(DATA_DIR, 'settings.json');
8
+
9
+ fs.ensureDirSync(DATA_DIR);
10
+
11
+ const DEFAULT_SETTINGS = {
12
+ interfaceParamMatcherEnabled: true,
13
+ responseParamMatcherEnabled: true,
14
+ };
15
+
16
+ const getSettings = () => {
17
+ try {
18
+ if (!fs.existsSync(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
19
+ return { ...DEFAULT_SETTINGS, ...fs.readJsonSync(SETTINGS_FILE) };
20
+ } catch (err) {
21
+ return { ...DEFAULT_SETTINGS };
22
+ }
23
+ };
24
+
25
+ const saveSettings = (settings) => {
26
+ fs.writeJsonSync(SETTINGS_FILE, settings, { spaces: 2 });
27
+ };
28
+
29
+ const initRouter = () => {
30
+ const router = express.Router();
31
+ router.use(express.json());
32
+
33
+ router.get('/settings', (req, res) => {
34
+ res.json(getSettings());
35
+ });
36
+
37
+ router.put('/settings', (req, res) => {
38
+ try {
39
+ const current = getSettings();
40
+ const updated = { ...current, ...req.body };
41
+ saveSettings(updated);
42
+ res.json(updated);
43
+ } catch (err) {
44
+ res.status(500).json({ error: '保存设置失败' });
45
+ }
46
+ });
47
+
48
+ return router;
49
+ };
50
+
51
+ module.exports = { initRouter, getSettings };
@@ -1,7 +1,24 @@
1
1
  const fs = require('fs');
2
+ const fse = require('fs-extra');
2
3
  const path = require('path');
3
4
  const url = require('url');
4
5
  const Mock = require('mockjs');
6
+ const storage = require('./storage');
7
+
8
+ const SETTINGS_FILE = path.join(storage.DATA_DIR, 'settings.json');
9
+
10
+ const getSettings = () => {
11
+ try {
12
+ if (!fse.existsSync(SETTINGS_FILE)) return { interfaceParamMatcherEnabled: true, responseParamMatcherEnabled: true };
13
+ const s = fse.readJsonSync(SETTINGS_FILE);
14
+ return {
15
+ interfaceParamMatcherEnabled: s.interfaceParamMatcherEnabled !== false,
16
+ responseParamMatcherEnabled: s.responseParamMatcherEnabled !== false,
17
+ };
18
+ } catch (e) {
19
+ return { interfaceParamMatcherEnabled: true, responseParamMatcherEnabled: true };
20
+ }
21
+ };
5
22
 
6
23
  // 获取嵌套对象属性值的工具函数
7
24
  const getNestedValue = (obj, path) => {
@@ -268,29 +285,34 @@ const ruleManager = {
268
285
  return null;
269
286
  }
270
287
 
271
- // 在URL匹配的接口中,查找参数也匹配的接口
272
- for (const intf of urlMatchedInterfaces) {
273
- // 检查参数匹配
274
- if (isParamMatch(req, intf.paramMatchers, this.log)) {
275
- this.log(`[规则处理器] 找到完全匹配的接口: ${intf.name} (URL + 参数匹配)`);
276
- return intf;
277
- } else {
278
- this.log(`[规则处理器] 接口 ${intf.name} URL匹配但参数不匹配,继续查找...`);
288
+ // 在URL匹配的接口中,查找参数也匹配的接口(受接口入参匹配开关控制)
289
+ const settings = getSettings();
290
+ if (settings.interfaceParamMatcherEnabled) {
291
+ for (const intf of urlMatchedInterfaces) {
292
+ if (isParamMatch(req, intf.paramMatchers, this.log)) {
293
+ this.log(`[规则处理器] 找到完全匹配的接口: ${intf.name} (URL + 参数匹配)`);
294
+ return intf;
295
+ } else {
296
+ this.log(`[规则处理器] 接口 ${intf.name} URL匹配但参数不匹配,继续查找...`);
297
+ }
279
298
  }
299
+
300
+ // 如果没有参数匹配的接口,返回第一个没有参数匹配规则的接口
301
+ const fallbackInterface = urlMatchedInterfaces.find(intf =>
302
+ !intf.paramMatchers || !Array.isArray(intf.paramMatchers) || intf.paramMatchers.length === 0
303
+ );
304
+
305
+ if (fallbackInterface) {
306
+ this.log(`[规则处理器] 使用回退接口: ${fallbackInterface.name} (无参数匹配规则)`);
307
+ return fallbackInterface;
308
+ }
309
+
310
+ this.log(`[规则处理器] 所有URL匹配的接口都有参数匹配规则且都不满足,未找到合适的接口`);
311
+ return null;
312
+ } else {
313
+ this.log('[规则处理器] 接口入参匹配已关闭,直接返回第一个URL匹配的接口');
314
+ return urlMatchedInterfaces[0];
280
315
  }
281
-
282
- // 如果没有参数匹配的接口,返回第一个没有参数匹配规则的接口
283
- const fallbackInterface = urlMatchedInterfaces.find(intf =>
284
- !intf.paramMatchers || !Array.isArray(intf.paramMatchers) || intf.paramMatchers.length === 0
285
- );
286
-
287
- if (fallbackInterface) {
288
- this.log(`[规则处理器] 使用回退接口: ${fallbackInterface.name} (无参数匹配规则)`);
289
- return fallbackInterface;
290
- }
291
-
292
- this.log(`[规则处理器] 所有URL匹配的接口都有参数匹配规则且都不满足,未找到合适的接口`);
293
- return null;
294
316
  },
295
317
 
296
318
  /**
@@ -437,9 +459,9 @@ const ruleManager = {
437
459
  }
438
460
  }
439
461
 
440
- // 对于有多个响应的接口,添加当前响应标识
441
- if (config.responses && config.activeResponseId) {
442
- const activeResponse = config.responses.find(r => r.id === config.activeResponseId);
462
+ // 对于有多个响应的接口,添加当前响应标识(优先展示实际命中的响应,在 handleDataTemplate 前此处只能用 activeResponseId 作为预估)
463
+ if (config.responses && config.responses.length > 0) {
464
+ const activeResponse = config.responses.find(r => r.id === config.activeResponseId) || config.responses[0];
443
465
  if (activeResponse && activeResponse.name) {
444
466
  res.setHeader('X-Whistle-Mock-Response', activeResponse.name);
445
467
  }
@@ -751,18 +773,63 @@ const ruleManager = {
751
773
 
752
774
  // 支持多响应系统
753
775
  let template = '';
776
+ let hitResponse = null;
754
777
 
755
778
  // 检查是否有多响应配置
756
779
  if (config.responses && Array.isArray(config.responses) && config.responses.length > 0) {
757
- // 获取当前激活的响应
758
- const activeResponseId = config.activeResponseId;
759
- const activeResponse = activeResponseId
760
- ? config.responses.find(r => r.id === activeResponseId)
761
- : config.responses[0];
762
-
763
- if (activeResponse) {
764
- this.log(`[规则处理器] 使用响应: ${activeResponse.name || '未命名'}`);
765
- template = activeResponse.content;
780
+ let selectedResponse = null;
781
+ const settings = getSettings();
782
+
783
+ // 1. 优先检查接口级 paramMatchers 中带 targetResponseId 的规则(受开关控制)
784
+ if (settings.interfaceParamMatcherEnabled) {
785
+ const interfaceMatchers = config.paramMatchers;
786
+ if (interfaceMatchers && Array.isArray(interfaceMatchers) && interfaceMatchers.length > 0) {
787
+ for (const matcher of interfaceMatchers) {
788
+ if (!matcher.targetResponseId) continue;
789
+ if (isParamMatch(req, [matcher], this.log)) {
790
+ const target = config.responses.find(r => r.id === matcher.targetResponseId);
791
+ if (target) {
792
+ selectedResponse = target;
793
+ this.log(`[规则处理器] 接口级规则指定响应匹配成功,使用响应: ${target.name || '未命名'}`);
794
+ break;
795
+ }
796
+ }
797
+ }
798
+ }
799
+ } else {
800
+ this.log('[规则处理器] 接口入参匹配已关闭,跳过接口级匹配');
801
+ }
802
+
803
+ // 2. 按顺序查找第一个响应级入参匹配的响应(受开关控制)
804
+ if (!selectedResponse) {
805
+ if (settings.responseParamMatcherEnabled) {
806
+ for (const resp of config.responses) {
807
+ if (resp.paramMatchers && Array.isArray(resp.paramMatchers) && resp.paramMatchers.length > 0) {
808
+ if (isParamMatch(req, resp.paramMatchers, this.log)) {
809
+ selectedResponse = resp;
810
+ this.log(`[规则处理器] 响应级入参匹配成功,使用响应: ${resp.name || '未命名'}`);
811
+ break;
812
+ } else {
813
+ this.log(`[规则处理器] 响应 "${resp.name || '未命名'}" 入参不匹配,继续检查下一个`);
814
+ }
815
+ }
816
+ }
817
+ } else {
818
+ this.log('[规则处理器] 响应入参匹配已关闭,跳过响应级匹配');
819
+ }
820
+ }
821
+
822
+ // 3. 无匹配则回退到 activeResponseId 指定的默认响应
823
+ if (!selectedResponse) {
824
+ const activeResponseId = config.activeResponseId;
825
+ selectedResponse = (activeResponseId && config.responses.find(r => r.id === activeResponseId))
826
+ || config.responses[0];
827
+ this.log(`[规则处理器] 无响应级匹配,使用默认响应: ${selectedResponse ? (selectedResponse.name || '未命名') : '无'}`);
828
+ }
829
+
830
+ if (selectedResponse) {
831
+ hitResponse = selectedResponse;
832
+ template = selectedResponse.content;
766
833
  }
767
834
  }
768
835
 
@@ -790,11 +857,10 @@ const ruleManager = {
790
857
  const enableMockHeaders = process.env.WHISTLE_MOCK_HEADERS !== 'false';
791
858
  if (enableMockHeaders) {
792
859
  res.setHeader('X-Whistle-Mock-Data-Type', 'template');
793
- if (config.responses && config.activeResponseId) {
794
- const activeResponse = config.responses.find(r => r.id === config.activeResponseId);
795
- if (activeResponse) {
796
- res.setHeader('X-Whistle-Mock-Template', activeResponse.name || 'unnamed');
797
- }
860
+ if (hitResponse) {
861
+ res.setHeader('X-Whistle-Mock-Template', hitResponse.name || 'unnamed');
862
+ // 覆盖 processRequest 中预设的响应头,确保反映实际命中的响应
863
+ res.setHeader('X-Whistle-Mock-Response', hitResponse.name || 'unnamed');
798
864
  }
799
865
  }
800
866
 
@@ -807,9 +873,7 @@ const ruleManager = {
807
873
  mockInfo: {
808
874
  delay: parseInt(config.responseDelay || config.delay || 0, 10),
809
875
  timestamp: new Date().toISOString(),
810
- responseName: config.responses && config.activeResponseId
811
- ? (config.responses.find(r => r.id === config.activeResponseId)?.name || '未命名')
812
- : '默认响应'
876
+ responseName: hitResponse ? (hitResponse.name || '未命名') : '默认响应'
813
877
  }
814
878
  };
815
879
  } catch (err) {
package/lib/server.js CHANGED
@@ -270,6 +270,10 @@ const app = express();
270
270
  app.use(express.json({limit: '100mb'}));
271
271
  app.use(express.urlencoded({ extended: true, limit: '100mb'}));
272
272
 
273
+ // 注册设置路由
274
+ const settingsRouter = require('./routes/settings');
275
+ app.use('/api', settingsRouter.initRouter());
276
+
273
277
  // 加载功能和接口配置
274
278
  const loadConfigurations = () => {
275
279
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whistle.mock-plugins",
3
- "version": "1.2.2-beta.2",
3
+ "version": "1.2.2-beta.23",
4
4
  "description": "Whistle 插件,用于快速创建 API 模拟数据",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -0,0 +1,115 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Drawer, Form, Input, message, Button, Space } from 'antd';
3
+ import axios from 'axios';
4
+ import { ResponseContentEditor, PreviewModal, formatResponseContent, refreshCacheAfterUpdate } from '.';
5
+
6
+ const DataManagementDrawer = ({ visible, onCancel, interfaceItem, onSaved }) => {
7
+ const [form] = Form.useForm();
8
+ const [previewVisible, setPreviewVisible] = useState(false);
9
+ const [previewContent, setPreviewContent] = useState(null);
10
+
11
+ useEffect(() => {
12
+ if (visible && interfaceItem) {
13
+ let responses = Array.isArray(interfaceItem.responses) ? interfaceItem.responses : [];
14
+ let activeResponseId = interfaceItem.activeResponseId || (responses[0] && responses[0].id) || '';
15
+ form.setFieldsValue({ responses, activeResponseId });
16
+ }
17
+ }, [visible, interfaceItem]);
18
+
19
+ const handlePreview = () => {
20
+ const values = form.getFieldsValue(true);
21
+ const responses = Array.isArray(values.responses) ? values.responses : [];
22
+ const activeResponse = responses.find(r => r.id === values.activeResponseId) || responses[0];
23
+ if (!activeResponse) {
24
+ message.error('未找到有效的响应内容');
25
+ return;
26
+ }
27
+ const contentType = 'application/json; charset=utf-8';
28
+ const formatted = formatResponseContent(activeResponse.content || '', contentType);
29
+ setPreviewContent({ title: `预览: ${activeResponse.name || '未命名响应'}`, content: formatted, contentType, description: '' });
30
+ setPreviewVisible(true);
31
+ };
32
+
33
+ const handleOk = async () => {
34
+ try {
35
+ await form.validateFields();
36
+ const values = form.getFieldsValue(true);
37
+ const payload = {
38
+ responses: Array.isArray(values.responses) ? values.responses : [],
39
+ activeResponseId: values.activeResponseId || (values.responses && values.responses[0] && values.responses[0].id) || ''
40
+ };
41
+ if (!interfaceItem || !interfaceItem.id) {
42
+ message.error('缺少接口ID,无法保存');
43
+ return;
44
+ }
45
+ const res = await axios.patch(`/cgi-bin/interfaces/${interfaceItem.id}`, payload);
46
+ if (res.data && res.data.code === 0) {
47
+ message.success('保存成功');
48
+ await refreshCacheAfterUpdate();
49
+ if (onSaved) onSaved();
50
+ if (onCancel) onCancel();
51
+ } else {
52
+ throw new Error(res.data?.message || '保存失败');
53
+ }
54
+ } catch (err) {
55
+ if (err && err.errorFields) return; // 表单校验错误
56
+ console.error('保存失败', err);
57
+ message.error(err.response?.data?.message || err.message || '保存失败');
58
+ }
59
+ };
60
+
61
+ return (
62
+ <>
63
+ <Drawer
64
+ title="数据管理"
65
+ placement="right"
66
+ width={1040}
67
+ open={visible}
68
+ onClose={onCancel}
69
+ destroyOnClose
70
+ bodyStyle={{ paddingBottom: 80 }}
71
+ footer={
72
+ <div style={{ textAlign: 'right' }}>
73
+ <Space>
74
+ <Button onClick={onCancel}>取消</Button>
75
+ <Button type="primary" onClick={handleOk}>保存</Button>
76
+ </Space>
77
+ </div>
78
+ }
79
+ >
80
+ <Form form={form} layout="vertical" initialValues={{ responses: [], activeResponseId: '' }}>
81
+ <Form.Item name="responses" hidden>
82
+ <Input />
83
+ </Form.Item>
84
+ <Form.Item name="activeResponseId" hidden>
85
+ <Input />
86
+ </Form.Item>
87
+ <Form.Item noStyle shouldUpdate>
88
+ {({ getFieldValue }) => {
89
+ const responses = getFieldValue('responses') || [];
90
+ const activeResponseId = getFieldValue('activeResponseId') || (responses[0] && responses[0].id) || '';
91
+ return (
92
+ <ResponseContentEditor
93
+ form={form}
94
+ responses={responses}
95
+ activeResponseId={activeResponseId}
96
+ onPreview={handlePreview}
97
+ enableSorting={true}
98
+ />
99
+ );
100
+ }}
101
+ </Form.Item>
102
+ </Form>
103
+ </Drawer>
104
+
105
+ <PreviewModal
106
+ visible={previewVisible}
107
+ onCancel={() => setPreviewVisible(false)}
108
+ previewContent={previewContent}
109
+ />
110
+ </>
111
+ );
112
+ };
113
+
114
+ export default DataManagementDrawer;
115
+