whistle.mock-plugins 1.2.1 → 1.2.2-beta.11

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.
@@ -152,6 +152,8 @@
152
152
 
153
153
  /**![menu](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTkwNCAxNjBIMTIwYy00LjQgMC04IDMuNi04IDh2NjRjMCA0LjQgMy42IDggOCA4aDc4NGM0LjQgMCA4LTMuNiA4LTh2LTY0YzAtNC40LTMuNi04LTgtOHptMCA2MjRIMTIwYy00LjQgMC04IDMuNi04IDh2NjRjMCA0LjQgMy42IDggOCA4aDc4NGM0LjQgMCA4LTMuNiA4LTh2LTY0YzAtNC40LTMuNi04LTgtOHptMC0zMTJIMTIwYy00LjQgMC04IDMuNi04IDh2NjRjMCA0LjQgMy42IDggOCA4aDc4NGM0LjQgMCA4LTMuNiA4LTh2LTY0YzAtNC40LTMuNi04LTgtOHoiIC8+PC9zdmc+) */
154
154
 
155
+ /**![minus-circle](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTY5NiA0ODBIMzI4Yy00LjQgMC04IDMuNi04IDh2NDhjMCA0LjQgMy42IDggOCA4aDM2OGM0LjQgMCA4LTMuNiA4LTh2LTQ4YzAtNC40LTMuNi04LTgtOHoiIC8+PHBhdGggZD0iTTUxMiA2NEMyNjQuNiA2NCA2NCAyNjQuNiA2NCA1MTJzMjAwLjYgNDQ4IDQ0OCA0NDggNDQ4LTIwMC42IDQ0OC00NDhTNzU5LjQgNjQgNTEyIDY0em0wIDgyMGMtMjA1LjQgMC0zNzItMTY2LjYtMzcyLTM3MnMxNjYuNi0zNzIgMzcyLTM3MiAzNzIgMTY2LjYgMzcyIDM3Mi0xNjYuNiAzNzItMzcyIDM3MnoiIC8+PC9zdmc+) */
156
+
155
157
  /**![minus-square](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMyOCA1NDRoMzY4YzQuNCAwIDgtMy42IDgtOHYtNDhjMC00LjQtMy42LTgtOC04SDMyOGMtNC40IDAtOCAzLjYtOCA4djQ4YzAgNC40IDMuNiA4IDggOHoiIC8+PHBhdGggZD0iTTg4MCAxMTJIMTQ0Yy0xNy43IDAtMzIgMTQuMy0zMiAzMnY3MzZjMCAxNy43IDE0LjMgMzIgMzIgMzJoNzM2YzE3LjcgMCAzMi0xNC4zIDMyLTMyVjE0NGMwLTE3LjctMTQuMy0zMi0zMi0zMnptLTQwIDcyOEgxODRWMTg0aDY1NnY2NTZ6IiAvPjwvc3ZnPg==) */
156
158
 
157
159
  /**![plus-circle](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTY5NiA0ODBINTQ0VjMyOGMwLTQuNC0zLjYtOC04LThoLTQ4Yy00LjQgMC04IDMuNi04IDh2MTUySDMyOGMtNC40IDAtOCAzLjYtOCA4djQ4YzAgNC40IDMuNiA4IDggOGgxNTJ2MTUyYzAgNC40IDMuNiA4IDggOGg0OGM0LjQgMCA4LTMuNiA4LThWNTQ0aDE1MmM0LjQgMCA4LTMuNiA4LTh2LTQ4YzAtNC40LTMuNi04LTgtOHoiIC8+PHBhdGggZD0iTTUxMiA2NEMyNjQuNiA2NCA2NCAyNjQuNiA2NCA1MTJzMjAwLjYgNDQ4IDQ0OCA0NDggNDQ4LTIwMC42IDQ0OC00NDhTNzU5LjQgNjQgNTEyIDY0em0wIDgyMGMtMjA1LjQgMC0zNzItMTY2LjYtMzcyLTM3MnMxNjYuNi0zNzIgMzcyLTM3MiAzNzIgMTY2LjYgMzcyIDM3Mi0xNjYuNiAzNzItMzcyIDM3MnoiIC8+PC9zdmc+) */
@@ -268,19 +268,36 @@ const ruleManager = {
268
268
  return null;
269
269
  }
270
270
 
271
- // 在URL匹配的接口中,查找参数也匹配的接口
272
- for (const intf of urlMatchedInterfaces) {
273
- // 检查参数匹配
271
+ // 在URL匹配的接口中进行参数匹配
272
+ // 新数据(响应内含 paramMatchers):接口级 paramMatchers 不参与接口筛选,直接返回第一个URL匹配的接口
273
+ // 旧数据(接口级 paramMatchers):保留原有逻辑,通过接口级 paramMatchers 筛选接口
274
+ const hasResponseLevelMatchers = (intf) => {
275
+ return intf.responses && Array.isArray(intf.responses) &&
276
+ intf.responses.some(r => r.paramMatchers && Array.isArray(r.paramMatchers) && r.paramMatchers.length > 0);
277
+ };
278
+
279
+ // 分离新旧数据接口
280
+ const newStyleInterfaces = urlMatchedInterfaces.filter(intf => hasResponseLevelMatchers(intf));
281
+ const legacyInterfaces = urlMatchedInterfaces.filter(intf => !hasResponseLevelMatchers(intf));
282
+
283
+ // 新数据接口:只要URL匹配就直接返回第一个(参数匹配在响应选择阶段处理)
284
+ if (newStyleInterfaces.length > 0) {
285
+ this.log(`[规则处理器] 找到新式接口(响应级匹配): ${newStyleInterfaces[0].name}`);
286
+ return newStyleInterfaces[0];
287
+ }
288
+
289
+ // 旧数据接口:保留原有接口级 paramMatchers 筛选逻辑
290
+ for (const intf of legacyInterfaces) {
274
291
  if (isParamMatch(req, intf.paramMatchers, this.log)) {
275
- this.log(`[规则处理器] 找到完全匹配的接口: ${intf.name} (URL + 参数匹配)`);
292
+ this.log(`[规则处理器] 找到完全匹配的接口: ${intf.name} (URL + 接口级参数匹配)`);
276
293
  return intf;
277
294
  } else {
278
295
  this.log(`[规则处理器] 接口 ${intf.name} URL匹配但参数不匹配,继续查找...`);
279
296
  }
280
297
  }
281
298
 
282
- // 如果没有参数匹配的接口,返回第一个没有参数匹配规则的接口
283
- const fallbackInterface = urlMatchedInterfaces.find(intf =>
299
+ // 旧数据兜底:返回第一个没有接口级参数匹配规则的接口
300
+ const fallbackInterface = legacyInterfaces.find(intf =>
284
301
  !intf.paramMatchers || !Array.isArray(intf.paramMatchers) || intf.paramMatchers.length === 0
285
302
  );
286
303
 
@@ -751,18 +768,38 @@ const ruleManager = {
751
768
 
752
769
  // 支持多响应系统
753
770
  let template = '';
771
+ let matchedResponse = null;
754
772
 
755
773
  // 检查是否有多响应配置
756
774
  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;
775
+ // 优先:遍历响应,找第一个 paramMatchers 匹配的响应(响应级匹配)
776
+ for (const resp of config.responses) {
777
+ if (resp.paramMatchers && Array.isArray(resp.paramMatchers) && resp.paramMatchers.length > 0) {
778
+ if (isParamMatch(req, resp.paramMatchers, this.log)) {
779
+ matchedResponse = resp;
780
+ this.log(`[规则处理器] 响应级参数匹配命中: ${resp.name || '未命名'}`);
781
+ break;
782
+ }
783
+ }
784
+ }
785
+
786
+ // 兜底:使用 activeResponseId 对应的响应,或第一个响应
787
+ if (!matchedResponse) {
788
+ const activeResponseId = config.activeResponseId;
789
+ matchedResponse = activeResponseId
790
+ ? config.responses.find(r => r.id === activeResponseId)
791
+ : null;
792
+
793
+ if (matchedResponse) {
794
+ this.log(`[规则处理器] 使用兜底响应(activeResponseId): ${matchedResponse.name || '未命名'}`);
795
+ } else {
796
+ matchedResponse = config.responses[0];
797
+ this.log(`[规则处理器] 使用第一个响应: ${matchedResponse?.name || '未命名'}`);
798
+ }
799
+ }
800
+
801
+ if (matchedResponse) {
802
+ template = matchedResponse.content;
766
803
  }
767
804
  }
768
805
 
@@ -790,11 +827,8 @@ const ruleManager = {
790
827
  const enableMockHeaders = process.env.WHISTLE_MOCK_HEADERS !== 'false';
791
828
  if (enableMockHeaders) {
792
829
  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
- }
830
+ if (matchedResponse) {
831
+ res.setHeader('X-Whistle-Mock-Template', matchedResponse.name || 'unnamed');
798
832
  }
799
833
  }
800
834
 
@@ -807,9 +841,7 @@ const ruleManager = {
807
841
  mockInfo: {
808
842
  delay: parseInt(config.responseDelay || config.delay || 0, 10),
809
843
  timestamp: new Date().toISOString(),
810
- responseName: config.responses && config.activeResponseId
811
- ? (config.responses.find(r => r.id === config.activeResponseId)?.name || '未命名')
812
- : '默认响应'
844
+ responseName: matchedResponse?.name || '默认响应'
813
845
  }
814
846
  };
815
847
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whistle.mock-plugins",
3
- "version": "1.2.1",
3
+ "version": "1.2.2-beta.11",
4
4
  "description": "Whistle 插件,用于快速创建 API 模拟数据",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { ReactSortable } from 'react-sortablejs';
3
3
  import ReactDOM from 'react-dom';
4
- import { Card, Button, Space, Badge, Row, Col, Input, Select, Tooltip, message, AutoComplete } from 'antd';
4
+ import { Card, Button, Space, Badge, Row, Col, Input, Select, Tooltip, message, AutoComplete, Tag } from 'antd';
5
5
  import {
6
6
  CodeOutlined,
7
7
  EyeOutlined,
@@ -13,7 +13,9 @@ import {
13
13
  EditOutlined,
14
14
  FullscreenOutlined,
15
15
  FullscreenExitOutlined,
16
- MenuOutlined
16
+ MenuOutlined,
17
+ FilterOutlined,
18
+ MinusCircleOutlined
17
19
  } from '@ant-design/icons';
18
20
  import { generateResponseId } from './utils';
19
21
 
@@ -89,7 +91,8 @@ const ResponseContentEditor = ({
89
91
  id: newResponseId,
90
92
  name: `响应 ${currentResponses.length + 1}`,
91
93
  description: '',
92
- content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
94
+ content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}',
95
+ paramMatchers: []
93
96
  };
94
97
 
95
98
  const updatedResponses = [...currentResponses, newResponse];
@@ -170,6 +173,42 @@ const ResponseContentEditor = ({
170
173
  }
171
174
  };
172
175
 
176
+ // 更新当前响应的 paramMatchers
177
+ const updateActiveResponseParamMatchers = (paramMatchers) => {
178
+ if (activeResponseIndex >= 0) {
179
+ const currentResponses = getFieldValue('responses') || [];
180
+ const newResponses = [...currentResponses];
181
+ newResponses[activeResponseIndex] = {
182
+ ...newResponses[activeResponseIndex],
183
+ paramMatchers
184
+ };
185
+ setFieldsValue({ responses: newResponses });
186
+ }
187
+ };
188
+
189
+ // 添加一条匹配规则
190
+ const handleAddMatcher = () => {
191
+ const currentMatchers = activeResponse?.paramMatchers || [];
192
+ updateActiveResponseParamMatchers([
193
+ ...currentMatchers,
194
+ { paramPath: '', paramValue: '', matchType: 'exact' }
195
+ ]);
196
+ };
197
+
198
+ // 删除一条匹配规则
199
+ const handleDeleteMatcher = (index) => {
200
+ const currentMatchers = [...(activeResponse?.paramMatchers || [])];
201
+ currentMatchers.splice(index, 1);
202
+ updateActiveResponseParamMatchers(currentMatchers);
203
+ };
204
+
205
+ // 更新某条匹配规则的字段
206
+ const handleMatcherChange = (index, field, value) => {
207
+ const currentMatchers = [...(activeResponse?.paramMatchers || [])];
208
+ currentMatchers[index] = { ...currentMatchers[index], [field]: value };
209
+ updateActiveResponseParamMatchers(currentMatchers);
210
+ };
211
+
173
212
  // 格式化JSON内容
174
213
  const formatJsonContent = () => {
175
214
  if (!activeResponse || !activeResponse.content) {
@@ -416,34 +455,48 @@ const ResponseContentEditor = ({
416
455
  onChange={handleResponseSelect}
417
456
  optionLabelProp="label"
418
457
  >
419
- {responses.map((resp, index) => (
420
- <Option
421
- key={resp.id}
422
- value={resp.id}
423
- label={
424
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
425
- <CheckCircleOutlined style={{ color: '#52c41a' }} />
426
- <span>{resp.name || `响应 ${index + 1}`}</span>
427
- </div>
428
- }
429
- >
430
- <div style={{ padding: '4px 0' }}>
431
- <div style={{
432
- display: 'flex',
433
- alignItems: 'center',
434
- justifyContent: 'space-between',
435
- marginBottom: '4px'
436
- }}>
437
- <span style={{ fontWeight: 'bold' }}>
438
- {resp.name || `响应 ${index + 1}`}
439
- </span>
440
- {resp.id === activeResponseId && (
441
- <Badge status="processing" text="当前选中" />
442
- )}
458
+ {responses.map((resp, index) => {
459
+ const matcherCount = resp.paramMatchers?.length || 0;
460
+ return (
461
+ <Option
462
+ key={resp.id}
463
+ value={resp.id}
464
+ label={
465
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
466
+ <CheckCircleOutlined style={{ color: '#52c41a' }} />
467
+ <span>{resp.name || `响应 ${index + 1}`}</span>
468
+ </div>
469
+ }
470
+ >
471
+ <div style={{ padding: '4px 0' }}>
472
+ <div style={{
473
+ display: 'flex',
474
+ alignItems: 'center',
475
+ justifyContent: 'space-between',
476
+ marginBottom: '4px'
477
+ }}>
478
+ <span style={{ fontWeight: 'bold' }}>
479
+ {resp.name || `响应 ${index + 1}`}
480
+ </span>
481
+ <Space size={4}>
482
+ {matcherCount > 0 ? (
483
+ <Tag color="blue" style={{ fontSize: '11px', margin: 0 }}>
484
+ {matcherCount}条规则
485
+ </Tag>
486
+ ) : (
487
+ <Tag color="default" style={{ fontSize: '11px', margin: 0 }}>
488
+ 兜底
489
+ </Tag>
490
+ )}
491
+ {resp.id === activeResponseId && (
492
+ <Badge status="processing" text="当前选中" />
493
+ )}
494
+ </Space>
495
+ </div>
443
496
  </div>
444
- </div>
445
- </Option>
446
- ))}
497
+ </Option>
498
+ );
499
+ })}
447
500
  </Select>
448
501
  <Tooltip title="编辑响应名称">
449
502
  <Button
@@ -484,6 +537,101 @@ const ResponseContentEditor = ({
484
537
  </Row>
485
538
  </div>
486
539
 
540
+ {/* 入参匹配规则编辑区域 */}
541
+ {activeResponse && (
542
+ <div style={{
543
+ marginBottom: '16px',
544
+ border: '1px solid #e8e8e8',
545
+ borderRadius: '6px',
546
+ padding: '12px',
547
+ background: '#fafafa'
548
+ }}>
549
+ <div style={{
550
+ display: 'flex',
551
+ alignItems: 'center',
552
+ justifyContent: 'space-between',
553
+ marginBottom: '10px'
554
+ }}>
555
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
556
+ <FilterOutlined style={{ color: '#1890ff' }} />
557
+ <label style={{ fontSize: '12px', color: '#333', fontWeight: 'bold' }}>
558
+ 入参匹配规则
559
+ </label>
560
+ {activeResponse.paramMatchers && activeResponse.paramMatchers.length > 0 ? (
561
+ <Tag color="blue" style={{ fontSize: '11px', lineHeight: '18px', padding: '0 6px' }}>
562
+ {activeResponse.paramMatchers.length}条
563
+ </Tag>
564
+ ) : (
565
+ <Tag color="default" style={{ fontSize: '11px', lineHeight: '18px', padding: '0 6px' }}>
566
+ 兜底响应
567
+ </Tag>
568
+ )}
569
+ </div>
570
+ <Button
571
+ type="dashed"
572
+ size="small"
573
+ icon={<PlusCircleOutlined />}
574
+ onClick={handleAddMatcher}
575
+ style={{ fontSize: '12px' }}
576
+ >
577
+ 添加规则
578
+ </Button>
579
+ </div>
580
+
581
+ {(!activeResponse.paramMatchers || activeResponse.paramMatchers.length === 0) && (
582
+ <div style={{
583
+ color: '#999',
584
+ fontSize: '12px',
585
+ textAlign: 'center',
586
+ padding: '8px 0'
587
+ }}>
588
+ 无匹配规则,当其他响应均不匹配时作为兜底返回
589
+ </div>
590
+ )}
591
+
592
+ {activeResponse.paramMatchers && activeResponse.paramMatchers.map((matcher, index) => (
593
+ <Row key={index} gutter={8} style={{ marginBottom: '8px' }} align="middle">
594
+ <Col span={8}>
595
+ <Input
596
+ size="small"
597
+ placeholder="参数路径,如 data.type"
598
+ value={matcher.paramPath}
599
+ onChange={(e) => handleMatcherChange(index, 'paramPath', e.target.value)}
600
+ />
601
+ </Col>
602
+ <Col span={5}>
603
+ <Select
604
+ size="small"
605
+ style={{ width: '100%' }}
606
+ value={matcher.matchType || 'exact'}
607
+ onChange={(val) => handleMatcherChange(index, 'matchType', val)}
608
+ >
609
+ <Option value="exact">精确匹配</Option>
610
+ <Option value="contains">包含</Option>
611
+ <Option value="regex">正则</Option>
612
+ </Select>
613
+ </Col>
614
+ <Col span={9}>
615
+ <Input
616
+ size="small"
617
+ placeholder="期望值"
618
+ value={matcher.paramValue}
619
+ onChange={(e) => handleMatcherChange(index, 'paramValue', e.target.value)}
620
+ />
621
+ </Col>
622
+ <Col span={2} style={{ textAlign: 'center' }}>
623
+ <Tooltip title="删除此规则">
624
+ <MinusCircleOutlined
625
+ style={{ color: '#ff4d4f', cursor: 'pointer', fontSize: '16px' }}
626
+ onClick={() => handleDeleteMatcher(index)}
627
+ />
628
+ </Tooltip>
629
+ </Col>
630
+ </Row>
631
+ ))}
632
+ </div>
633
+ )}
634
+
487
635
  {/* 响应内容编辑区域 */}
488
636
  {activeResponse && (
489
637
  <>