whistle.mock-plugins 1.0.4 → 1.0.5

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.
@@ -127,6 +127,8 @@
127
127
 
128
128
  /**![menu-unfold](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTQwOCA0NDJoNDgwYzQuNCAwIDgtMy42IDgtOHYtNTZjMC00LjQtMy42LTgtOC04SDQwOGMtNC40IDAtOCAzLjYtOCA4djU2YzAgNC40IDMuNiA4IDggOHptLTggMjA0YzAgNC40IDMuNiA4IDggOGg0ODBjNC40IDAgOC0zLjYgOC04di01NmMwLTQuNC0zLjYtOC04LThINDA4Yy00LjQgMC04IDMuNi04IDh2NTZ6bTUwNC00ODZIMTIwYy00LjQgMC04IDMuNi04IDh2NTZjMCA0LjQgMy42IDggOCA4aDc4NGM0LjQgMCA4LTMuNiA4LTh2LTU2YzAtNC40LTMuNi04LTgtOHptMCA2MzJIMTIwYy00LjQgMC04IDMuNi04IDh2NTZjMCA0LjQgMy42IDggOCA4aDc4NGM0LjQgMCA4LTMuNiA4LTh2LTU2YzAtNC40LTMuNi04LTgtOHpNMTQyLjQgNjQyLjFMMjk4LjcgNTE5YTguODQgOC44NCAwIDAwMC0xMy45TDE0Mi40IDM4MS45Yy01LjgtNC42LTE0LjQtLjUtMTQuNCA2Ljl2MjQ2LjNhOC45IDguOSAwIDAwMTQuNCA3eiIgLz48L3N2Zz4=) */
129
129
 
130
+ /**![minus-circle](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTY5NiA0ODBIMzI4Yy00LjQgMC04IDMuNi04IDh2NDhjMCA0LjQgMy42IDggOCA4aDM2OGM0LjQgMCA4LTMuNiA4LTh2LTQ4YzAtNC40LTMuNi04LTgtOHoiIC8+PHBhdGggZD0iTTUxMiA2NEMyNjQuNiA2NCA2NCAyNjQuNiA2NCA1MTJzMjAwLjYgNDQ4IDQ0OCA0NDggNDQ4LTIwMC42IDQ0OC00NDhTNzU5LjQgNjQgNTEyIDY0em0wIDgyMGMtMjA1LjQgMC0zNzItMTY2LjYtMzcyLTM3MnMxNjYuNi0zNzIgMzcyLTM3MiAzNzIgMTY2LjYgMzcyIDM3Mi0xNjYuNiAzNzItMzcyIDM3MnoiIC8+PC9zdmc+) */
131
+
130
132
  /**![minus-square](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMyOCA1NDRoMzY4YzQuNCAwIDgtMy42IDgtOHYtNDhjMC00LjQtMy42LTgtOC04SDMyOGMtNC40IDAtOCAzLjYtOCA4djQ4YzAgNC40IDMuNiA4IDggOHoiIC8+PHBhdGggZD0iTTg4MCAxMTJIMTQ0Yy0xNy43IDAtMzIgMTQuMy0zMiAzMnY3MzZjMCAxNy43IDE0LjMgMzIgMzIgMzJoNzM2YzE3LjcgMCAzMi0xNC4zIDMyLTMyVjE0NGMwLTE3LjctMTQuMy0zMi0zMi0zMnptLTQwIDcyOEgxODRWMTg0aDY1NnY2NTZ6IiAvPjwvc3ZnPg==) */
131
133
 
132
134
  /**![play-circle](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTUxMiA2NEMyNjQuNiA2NCA2NCAyNjQuNiA2NCA1MTJzMjAwLjYgNDQ4IDQ0OCA0NDggNDQ4LTIwMC42IDQ0OC00NDhTNzU5LjQgNjQgNTEyIDY0em0wIDgyMGMtMjA1LjQgMC0zNzItMTY2LjYtMzcyLTM3MnMxNjYuNi0zNzIgMzcyLTM3MiAzNzIgMTY2LjYgMzcyIDM3Mi0xNjYuNiAzNzItMzcyIDM3MnoiIC8+PHBhdGggZD0iTTcxOS40IDQ5OS4xbC0yOTYuMS0yMTVBMTUuOSAxNS45IDAgMDAzOTggMjk3djQzMGMwIDEzLjEgMTQuOCAyMC41IDI1LjMgMTIuOWwyOTYuMS0yMTVhMTUuOSAxNS45IDAgMDAwLTI1Ljh6bS0yNTcuNiAxMzRWMzkwLjlMNjI4LjUgNTEyIDQ2MS44IDYzMy4xeiIgLz48L3N2Zz4=) */
@@ -192,6 +192,50 @@ const validateRedirectRule = (interface) => {
192
192
  return true;
193
193
  };
194
194
 
195
+ // 生成随机数工具函数
196
+ const generateRandomValue = (pattern) => {
197
+ // 如果不是以@开头的模式,直接返回原值
198
+ if (!pattern || !pattern.startsWith('@')) {
199
+ return pattern;
200
+ }
201
+
202
+ const formatPattern = pattern.substring(1); // 去掉@前缀
203
+ // 为每个x生成一个随机字符(字母或数字)
204
+ return formatPattern.replace(/x/g, () => {
205
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
206
+ return chars.charAt(Math.floor(Math.random() * chars.length));
207
+ });
208
+ };
209
+
210
+ // 构建重定向规则
211
+ const buildRedirectRule = (fullUrl, targetUrl, customHeaders, pattern, isUrlRedirect = 0) => {
212
+ if(!isUrlRedirect){
213
+ targetUrl = fullUrl.replace(pattern, targetUrl);
214
+ }else{
215
+ targetUrl = `redirect://${targetUrl}`;
216
+ }
217
+
218
+ let rule = `${fullUrl} ${targetUrl}`;
219
+
220
+ // 如果有自定义请求头,添加到规则中
221
+ if (customHeaders && Object.keys(customHeaders).length > 0) {
222
+ // 构建headerReplace规则,格式: headerReplace://req.header-name:pattern1=replacement1&pattern2=replacement2
223
+ const headerRuleLines = Object.entries(customHeaders).map(([key, value]) => {
224
+ // 处理随机值
225
+ const processedValue = generateRandomValue(value);
226
+
227
+ return `${fullUrl} headerReplace://req.${key}:/(.*)/=${processedValue}`;
228
+ });
229
+
230
+ if (headerRuleLines.length > 0) {
231
+ // 将所有规则合并为一个多行规则字符串
232
+ rule = `${rule}\n${headerRuleLines.join('\n')}`;
233
+ }
234
+ }
235
+
236
+ return rule;
237
+ };
238
+
195
239
  // Whistle 规则服务器实现
196
240
  module.exports = (server, options) => {
197
241
  server.on('request', (req, res) => {
@@ -337,12 +381,15 @@ module.exports = (server, options) => {
337
381
  // 执行重定向:将fullUrl中匹配的pattern替换为targetUrl
338
382
  // 对于redirect类型,我们使用indexOf确认前缀匹配
339
383
  if (fullUrl.indexOf(pattern) === 0) {
340
- // 构建新的目标URL
341
- const redirectUrl = fullUrl.replace(pattern, targetUrl);
384
+ // 获取自定义请求头
385
+ const customHeaders = matchedInterface.customHeaders || {};
386
+
387
+ // 构建重定向规则,包括自定义请求头
388
+ const redirectRule = buildRedirectRule(fullUrl, targetUrl, customHeaders, pattern, 0);
342
389
 
343
- // 按照 whistle 规则格式返回重定向规则
344
- res.end(`${fullUrl} ${redirectUrl}`);
345
- log(`重定向规则: ${fullUrl} ${redirectUrl}`);
390
+ // 返回规则
391
+ res.end(redirectRule);
392
+ log(`重定向规则: ${redirectRule}`);
346
393
  return;
347
394
  } else {
348
395
  log(`URL ${fullUrl} 不以 ${pattern} 开头,跳过处理`);
@@ -365,9 +412,15 @@ module.exports = (server, options) => {
365
412
 
366
413
  // 对于url_redirect,我们需要完全匹配
367
414
  if (fullUrl === matchedInterface.urlPattern) {
368
- // 直接返回whistle规则
369
- res.end(`${fullUrl} redirect://${targetUrl}`);
370
- log(`URL重定向规则: ${fullUrl} redirect://${targetUrl}`);
415
+ // 获取自定义请求头
416
+ const customHeaders = matchedInterface.customHeaders || {};
417
+
418
+ // 构建重定向规则,包括自定义请求头
419
+ const redirectRule = buildRedirectRule(fullUrl, targetUrl, customHeaders, pattern, 1);
420
+
421
+ // 返回规则
422
+ res.end(redirectRule);
423
+ log(`URL重定向规则: ${redirectRule}`);
371
424
  return;
372
425
  } else {
373
426
  log(`URL ${fullUrl} 不完全匹配 ${matchedInterface.urlPattern},跳过处理`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whistle.mock-plugins",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Whistle 插件,用于快速创建 API 模拟数据",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -1,8 +1,8 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React, { useState, useEffect, useRef } from 'react';
2
2
  import { useParams, useHistory } from 'react-router-dom';
3
3
  import AppLayout from '../components/AppLayout';
4
- import { Table, Button, Modal, Form, Input, Select, message, Switch, Popconfirm, Alert, Space, Tabs } from 'antd';
5
- import { PlusOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined, PlayCircleOutlined, FormatPainterOutlined, EyeOutlined } from '@ant-design/icons';
4
+ import { Table, Button, Modal, Form, Input, Select, message, Switch, Popconfirm, Alert, Space, Tabs, Card, Divider, Badge, Tooltip, Row, Col } from 'antd';
5
+ import { PlusOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined, PlayCircleOutlined, FormatPainterOutlined, EyeOutlined, CopyOutlined, CodeOutlined, CheckCircleOutlined, CloseCircleOutlined, MenuOutlined, PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons';
6
6
  import '../styles/interface-management.css';
7
7
  import axios from 'axios';
8
8
 
@@ -137,6 +137,16 @@ const InterfaceManagement = () => {
137
137
 
138
138
  const handleEditInterface = (record) => {
139
139
  setEditingInterface(record);
140
+
141
+ // 将自定义请求头转换为数组格式,用于动态表单项
142
+ let headersArray = [];
143
+ if (record.customHeaders && typeof record.customHeaders === 'object') {
144
+ headersArray = Object.entries(record.customHeaders).map(([key, value]) => ({
145
+ headerName: key,
146
+ headerValue: value
147
+ }));
148
+ }
149
+
140
150
  form.setFieldsValue({
141
151
  name: record.name,
142
152
  pattern: record.urlPattern,
@@ -146,6 +156,7 @@ const InterfaceManagement = () => {
146
156
  responseBody: record.responseContent || '',
147
157
  httpMethod: record.httpMethod || 'ALL',
148
158
  targetUrl: record.targetUrl || '',
159
+ headerItems: headersArray // 使用数组存储表单项
149
160
  });
150
161
  setModalVisible(true);
151
162
  };
@@ -207,29 +218,27 @@ const InterfaceManagement = () => {
207
218
  try {
208
219
  const values = await form.validateFields();
209
220
 
210
- // 验证响应内容格式
211
- if (values.proxyType === 'response' && values.contentType.includes('json')) {
221
+ // 检查responseBody是否是有效的JSON
222
+ if (values.proxyType === 'response' && values.contentType.includes('application/json')) {
212
223
  try {
213
- // 清理并验证JSON
214
- const cleanedJson = cleanJsonResponse(values.responseBody);
215
- JSON.parse(cleanedJson);
216
- values.responseBody = cleanedJson;
224
+ JSON.parse(values.responseBody);
217
225
  } catch (e) {
218
- message.error('JSON响应内容格式不正确');
226
+ message.error('响应体不是有效的JSON格式');
219
227
  return;
220
228
  }
221
229
  }
222
230
 
223
- // 验证重定向URL
224
- if ((values.proxyType === 'redirect' || values.proxyType === 'url_redirect') && !values.targetUrl) {
225
- message.error('重定向目标URL不能为空');
226
- return;
227
- }
231
+ // 处理自定义请求头,将表单项数组转换为对象格式
232
+ let customHeaders = {};
228
233
 
229
- // 验证重定向URL的格式
230
- if ((values.proxyType === 'redirect' || values.proxyType === 'url_redirect') && !isValidUrl(values.targetUrl)) {
231
- message.error('重定向目标URL格式不正确,请输入完整的URL,包括http://或https://');
232
- return;
234
+ if (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') {
235
+ if (values.headerItems && values.headerItems.length > 0) {
236
+ values.headerItems.forEach(item => {
237
+ if (item && item.headerName && item.headerName.trim()) {
238
+ customHeaders[item.headerName.trim()] = item.headerValue || '';
239
+ }
240
+ });
241
+ }
233
242
  }
234
243
 
235
244
  const interfaceData = {
@@ -239,6 +248,7 @@ const InterfaceManagement = () => {
239
248
  proxyType: values.proxyType,
240
249
  responseContent: values.proxyType === 'response' ? values.responseBody : '',
241
250
  targetUrl: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? values.targetUrl : '',
251
+ customHeaders: (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') ? customHeaders : {},
242
252
  httpStatus: parseInt(values.statusCode, 10), // 转换为数字
243
253
  contentType: values.contentType,
244
254
  responseDelay: 0,
@@ -516,6 +526,47 @@ const InterfaceManagement = () => {
516
526
  return (record.proxyType === 'redirect' || record.proxyType === 'url_redirect') ? text : '-';
517
527
  }
518
528
  },
529
+ {
530
+ title: '自定义头',
531
+ dataIndex: 'customHeaders',
532
+ key: 'customHeaders',
533
+ width: 100,
534
+ render: (_, record) => {
535
+ if (record.proxyType !== 'redirect' && record.proxyType !== 'url_redirect') {
536
+ return '-';
537
+ }
538
+
539
+ const headers = record.customHeaders || {};
540
+ const count = Object.keys(headers).length;
541
+
542
+ if (count === 0) {
543
+ return '-';
544
+ }
545
+
546
+ // 检查是否包含随机值
547
+ const hasRandomValue = Object.values(headers).some(v => v && v.startsWith('@'));
548
+
549
+ return (
550
+ <Tooltip title={
551
+ <div>
552
+ {Object.entries(headers).map(([key, value]) => (
553
+ <div key={key}>
554
+ {key}: {value}
555
+ {value && value.startsWith('@') && (
556
+ <span style={{ color: '#52c41a' }}> (随机)</span>
557
+ )}
558
+ </div>
559
+ ))}
560
+ </div>
561
+ }>
562
+ <span style={{ color: '#1890ff' }}>
563
+ {count}个
564
+ {hasRandomValue && <span style={{ marginLeft: 4 }}>🎲</span>}
565
+ </span>
566
+ </Tooltip>
567
+ );
568
+ }
569
+ },
519
570
  {
520
571
  title: '请求方法',
521
572
  dataIndex: 'httpMethod',
@@ -640,7 +691,8 @@ const InterfaceManagement = () => {
640
691
  contentType: 'application/json; charset=utf-8',
641
692
  responseBody: '{\n "code": 0,\n "message": "success",\n "data": {}\n}',
642
693
  httpMethod: 'ALL',
643
- targetUrl: ''
694
+ targetUrl: '',
695
+ headerItems: []
644
696
  }}
645
697
  >
646
698
  <div style={{ display: 'flex', flexDirection: 'row', gap: '16px' }}>
@@ -866,51 +918,183 @@ const InterfaceManagement = () => {
866
918
 
867
919
  if (proxyType === 'redirect' || proxyType === 'url_redirect') {
868
920
  return (
869
- <Form.Item
870
- name="targetUrl"
871
- label="重定向目标URL"
872
- rules={[
873
- { required: true, message: '请输入重定向目标URL' },
874
- {
875
- validator: (_, value) => {
876
- if (!value) return Promise.resolve();
877
-
878
- try {
879
- new URL(value);
880
- return Promise.resolve();
881
- } catch (e) {
882
- return Promise.reject(new Error('请输入有效的URL,必须包含http://或https://'));
921
+ <>
922
+ <Form.Item
923
+ name="targetUrl"
924
+ label="重定向目标URL"
925
+ rules={[
926
+ { required: true, message: '请输入重定向目标URL' },
927
+ {
928
+ validator: (_, value) => {
929
+ if (!value) return Promise.resolve();
930
+
931
+ try {
932
+ new URL(value);
933
+ return Promise.resolve();
934
+ } catch (e) {
935
+ return Promise.reject(new Error('请输入有效的URL,必须包含http://或https://'));
936
+ }
883
937
  }
884
938
  }
939
+ ]}
940
+ tooltip={proxyType === 'redirect' ?
941
+ "重定向模式:输入完整的目标URL。匹配时使用前缀匹配,只要请求URL以匹配规则开头即命中。例如:https://example.com/api" :
942
+ "URL重定向模式:输入完整的目标URL。匹配时要求完全匹配URL,必须与匹配规则完全一致才命中。例如:https://example.com/api/users"
885
943
  }
886
- ]}
887
- tooltip={proxyType === 'redirect' ?
888
- "重定向模式:输入完整的目标URL。匹配时使用前缀匹配,只要请求URL以匹配规则开头即命中。例如:https://example.com/api" :
889
- "URL重定向模式:输入完整的目标URL。匹配时要求完全匹配URL,必须与匹配规则完全一致才命中。例如:https://example.com/api/users"
890
- }
891
- >
892
- <Input
893
- placeholder={proxyType === 'redirect' ?
894
- "例如:https://example.com/api" :
895
- "例如:https://example.com/api/users"
896
- }
897
- addonBefore={
898
- <Select
899
- defaultValue="https://"
900
- className="select-before"
901
- style={{ width: 100 }}
902
- onChange={(value) => {
903
- const currentUrl = form.getFieldValue('targetUrl') || '';
904
- const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, '');
905
- form.setFieldsValue({ targetUrl: value + urlWithoutProtocol });
906
- }}
907
- >
908
- <Option value="http://">http://</Option>
909
- <Option value="https://">https://</Option>
910
- </Select>
911
- }
912
- />
913
- </Form.Item>
944
+ >
945
+ <Input
946
+ placeholder={proxyType === 'redirect' ?
947
+ "例如:https://example.com/api" :
948
+ "例如:https://example.com/api/users"
949
+ }
950
+ addonBefore={
951
+ <Select
952
+ defaultValue="https://"
953
+ className="select-before"
954
+ style={{ width: 100 }}
955
+ onChange={(value) => {
956
+ const currentUrl = form.getFieldValue('targetUrl') || '';
957
+ const urlWithoutProtocol = currentUrl.replace(/^https?:\/\//, '');
958
+ form.setFieldsValue({ targetUrl: value + urlWithoutProtocol });
959
+ }}
960
+ >
961
+ <Option value="http://">http://</Option>
962
+ <Option value="https://">https://</Option>
963
+ </Select>
964
+ }
965
+ />
966
+ </Form.Item>
967
+
968
+ <Form.Item
969
+ label="自定义请求头"
970
+ tooltip="为请求添加或替换HTTP请求头"
971
+ className="custom-headers-container"
972
+ >
973
+ <Form.List name="headerItems">
974
+ {(fields, { add, remove }) => (
975
+ <>
976
+ {fields.map(({ key, name, ...restField }) => (
977
+ <Row key={key} gutter={8} style={{ marginBottom: 8 }}>
978
+ <Col span={10}>
979
+ <Form.Item
980
+ {...restField}
981
+ name={[name, 'headerName']}
982
+ noStyle
983
+ rules={[
984
+ {
985
+ required: true,
986
+ message: '请输入请求头名称'
987
+ },
988
+ {
989
+ pattern: /^[^:]+$/,
990
+ message: '请求头名称不能包含冒号'
991
+ }
992
+ ]}
993
+ >
994
+ <Input placeholder="Header-Name" />
995
+ </Form.Item>
996
+ </Col>
997
+ <Col span={12}>
998
+ <Form.Item
999
+ {...restField}
1000
+ name={[name, 'headerValue']}
1001
+ noStyle
1002
+ rules={[
1003
+ {
1004
+ validator: (_, value) => {
1005
+ if (!value) return Promise.resolve();
1006
+
1007
+ // 检查是否是随机数格式 @xxxx-xxx
1008
+ if (value.startsWith('@')) {
1009
+ const randomPattern = value.substring(1);
1010
+ // 验证格式: 只能包含x和-,且首尾不能是-
1011
+ if (!/^[x][-x]*[x]$/.test(randomPattern) ||
1012
+ randomPattern.startsWith('-') ||
1013
+ randomPattern.endsWith('-')) {
1014
+ return Promise.reject(
1015
+ new Error('随机数格式错误,应为@xxxx-xxx格式,只能包含x和-,且首尾不能是-')
1016
+ );
1017
+ }
1018
+ }
1019
+ return Promise.resolve();
1020
+ }
1021
+ }
1022
+ ]}
1023
+ >
1024
+ <Input placeholder="Header-Value" />
1025
+ </Form.Item>
1026
+ </Col>
1027
+ <Col span={2}>
1028
+ <Button
1029
+ type="text"
1030
+ icon={<MinusCircleOutlined />}
1031
+ onClick={() => remove(name)}
1032
+ style={{ color: '#ff4d4f' }}
1033
+ />
1034
+ </Col>
1035
+ </Row>
1036
+ ))}
1037
+
1038
+ <Form.Item>
1039
+ <Button
1040
+ type="dashed"
1041
+ onClick={() => add()}
1042
+ block
1043
+ icon={<PlusOutlined />}
1044
+ >
1045
+ 添加请求头
1046
+ </Button>
1047
+ </Form.Item>
1048
+
1049
+ <div style={{ marginTop: 8 }}>
1050
+ <Button
1051
+ type="link"
1052
+ onClick={() => {
1053
+ add({ headerName: 'Content-Type', headerValue: 'application/json' });
1054
+ }}
1055
+ >
1056
+ 添加 Content-Type
1057
+ </Button>
1058
+ <Button
1059
+ type="link"
1060
+ onClick={() => {
1061
+ add({ headerName: 'Authorization', headerValue: 'Bearer ' });
1062
+ }}
1063
+ >
1064
+ 添加 Authorization
1065
+ </Button>
1066
+ <Button
1067
+ type="link"
1068
+ onClick={() => {
1069
+ add({ headerName: 'User-Agent', headerValue: 'Mozilla/5.0' });
1070
+ }}
1071
+ >
1072
+ 添加 User-Agent
1073
+ </Button>
1074
+ <Button
1075
+ type="link"
1076
+ onClick={() => {
1077
+ add({ headerName: 'X-Random-ID', headerValue: '@xxxx-xxxx' });
1078
+ }}
1079
+ >
1080
+ 添加随机ID
1081
+ </Button>
1082
+ </div>
1083
+
1084
+ <div style={{ marginTop: 8, background: '#f5f5f5', padding: '8px', borderRadius: '4px' }}>
1085
+ <div style={{ marginBottom: '8px', fontWeight: 'bold' }}>随机数格式说明:</div>
1086
+ <ul style={{ margin: 0, paddingLeft: '20px' }}>
1087
+ <li>使用 <code>@</code> 开头表示这是一个随机数值</li>
1088
+ <li>格式示例:<code>@xxxx-xxxx</code> 将生成如 <code>a1b2-c3d4</code> 的随机值</li>
1089
+ <li>每个 <code>x</code> 将替换为随机字母或数字</li>
1090
+ <li><code>-</code> 将保留在输出中作为分隔符</li>
1091
+ </ul>
1092
+ </div>
1093
+ </>
1094
+ )}
1095
+ </Form.List>
1096
+ </Form.Item>
1097
+ </>
914
1098
  );
915
1099
  }
916
1100
 
@@ -1046,6 +1230,34 @@ const InterfaceManagement = () => {
1046
1230
  </div>
1047
1231
  )}
1048
1232
 
1233
+ {(editingInterface.proxyType === 'redirect' || editingInterface.proxyType === 'url_redirect') &&
1234
+ testResult.formattedHeaders && (
1235
+ <div className="result-item">
1236
+ <span className="label">自定义请求头:</span>
1237
+ <div className="value" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>
1238
+ {
1239
+ editingInterface.customHeaders &&
1240
+ Object.entries(editingInterface.customHeaders)
1241
+ .map(([key, value]) => {
1242
+ const generatedValue = testResult.customHeaders[key];
1243
+ return (
1244
+ <div key={key}>
1245
+ {key}: {value.startsWith('@') ? (
1246
+ <span>
1247
+ <span style={{ color: '#d9d9d9' }}>{value}</span>
1248
+ {' '} → {' '}
1249
+ <span style={{ color: '#52c41a', fontWeight: 'bold' }}>{generatedValue}</span>
1250
+ <span style={{ color: '#8c8c8c', fontSize: '12px' }}> (随机生成)</span>
1251
+ </span>
1252
+ ) : generatedValue}
1253
+ </div>
1254
+ );
1255
+ })
1256
+ }
1257
+ </div>
1258
+ </div>
1259
+ )}
1260
+
1049
1261
  {editingInterface.proxyType === 'response' && (
1050
1262
  <>
1051
1263
  <div className="result-item">