timigogo commited on
Commit
2e68e7f
·
verified ·
1 Parent(s): 4c4a33a

Upload 110 files

Browse files
Files changed (1) hide show
  1. web/src/pages/Channel/EditChannel.js +1114 -1081
web/src/pages/Channel/EditChannel.js CHANGED
@@ -1,1081 +1,1114 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { useNavigate, useParams } from 'react-router-dom';
3
- import { useTranslation } from 'react-i18next';
4
- import {
5
- API,
6
- isMobile,
7
- showError,
8
- showInfo,
9
- showSuccess, showWarning,
10
- verifyJSON
11
- } from '../../helpers';
12
- import { CHANNEL_OPTIONS } from '../../constants';
13
- import Title from '@douyinfe/semi-ui/lib/es/typography/title';
14
- import {
15
- SideSheet,
16
- Space,
17
- Spin,
18
- Button,
19
- Tooltip,
20
- Input,
21
- Typography,
22
- Select,
23
- TextArea,
24
- Checkbox,
25
- Banner, Modal
26
- } from '@douyinfe/semi-ui';
27
- import { getChannelModels, loadChannelModels } from '../../components/utils.js';
28
-
29
- const MODEL_MAPPING_EXAMPLE = {
30
- 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
31
- };
32
-
33
- const STATUS_CODE_MAPPING_EXAMPLE = {
34
- 400: '500'
35
- };
36
-
37
- const REGION_EXAMPLE = {
38
- 'default': 'us-central1',
39
- 'claude-3-5-sonnet-20240620': 'europe-west1'
40
- };
41
-
42
- function type2secretPrompt(type) {
43
- // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
44
- switch (type) {
45
- case 15:
46
- return '按照如下格式输入:APIKey|SecretKey';
47
- case 18:
48
- return '按照如下格式输入:APPID|APISecret|APIKey';
49
- case 22:
50
- return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
51
- case 23:
52
- return '按照如下格式输入:AppId|SecretId|SecretKey';
53
- case 33:
54
- return '按照如下格式输入:Ak|Sk|Region';
55
- default:
56
- return '请输入渠道对应的鉴权密钥';
57
- }
58
- }
59
-
60
- const EditChannel = (props) => {
61
- const { t } = useTranslation();
62
- const navigate = useNavigate();
63
- const channelId = props.editingChannel.id;
64
- const isEdit = channelId !== undefined;
65
- const [loading, setLoading] = useState(isEdit);
66
- const handleCancel = () => {
67
- props.handleClose();
68
- };
69
- const originInputs = {
70
- name: '',
71
- type: 1,
72
- key: '',
73
- openai_organization: '',
74
- max_input_tokens: 0,
75
- base_url: '',
76
- other: '',
77
- model_mapping: '',
78
- status_code_mapping: '',
79
- models: [],
80
- auto_ban: 1,
81
- test_model: '',
82
- groups: ['default'],
83
- priority: 0,
84
- weight: 0,
85
- tag: ''
86
- };
87
- const [batch, setBatch] = useState(false);
88
- const [autoBan, setAutoBan] = useState(true);
89
- // const [autoBan, setAutoBan] = useState(true);
90
- const [inputs, setInputs] = useState(originInputs);
91
- const [originModelOptions, setOriginModelOptions] = useState([]);
92
- const [modelOptions, setModelOptions] = useState([]);
93
- const [groupOptions, setGroupOptions] = useState([]);
94
- const [basicModels, setBasicModels] = useState([]);
95
- const [fullModels, setFullModels] = useState([]);
96
- const [customModel, setCustomModel] = useState('');
97
- const handleInputChange = (name, value) => {
98
- if (name === 'base_url' && value.endsWith('/v1')) {
99
- Modal.confirm({
100
- title: '警告',
101
- content: '不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
102
- onOk: () => {
103
- setInputs((inputs) => ({ ...inputs, [name]: value }));
104
- }
105
- })
106
- return
107
- }
108
- setInputs((inputs) => ({ ...inputs, [name]: value }));
109
- if (name === 'type') {
110
- let localModels = [];
111
- switch (value) {
112
- case 2:
113
- localModels = [
114
- 'mj_imagine',
115
- 'mj_variation',
116
- 'mj_reroll',
117
- 'mj_blend',
118
- 'mj_upscale',
119
- 'mj_describe',
120
- 'mj_uploads'
121
- ];
122
- break;
123
- case 5:
124
- localModels = [
125
- 'swap_face',
126
- 'mj_imagine',
127
- 'mj_variation',
128
- 'mj_reroll',
129
- 'mj_blend',
130
- 'mj_upscale',
131
- 'mj_describe',
132
- 'mj_zoom',
133
- 'mj_shorten',
134
- 'mj_modal',
135
- 'mj_inpaint',
136
- 'mj_custom_zoom',
137
- 'mj_high_variation',
138
- 'mj_low_variation',
139
- 'mj_pan',
140
- 'mj_uploads'
141
- ];
142
- break;
143
- case 36:
144
- localModels = [
145
- 'suno_music',
146
- 'suno_lyrics'
147
- ];
148
- break;
149
- default:
150
- localModels = getChannelModels(value);
151
- break;
152
- }
153
- if (inputs.models.length === 0) {
154
- setInputs((inputs) => ({ ...inputs, models: localModels }));
155
- }
156
- setBasicModels(localModels);
157
- }
158
- //setAutoBan
159
- };
160
-
161
- const loadChannel = async () => {
162
- setLoading(true);
163
- let res = await API.get(`/api/channel/${channelId}`);
164
- if (res === undefined) {
165
- return;
166
- }
167
- const { success, message, data } = res.data;
168
- if (success) {
169
- if (data.models === '') {
170
- data.models = [];
171
- } else {
172
- data.models = data.models.split(',');
173
- }
174
- if (data.group === '') {
175
- data.groups = [];
176
- } else {
177
- data.groups = data.group.split(',');
178
- }
179
- if (data.model_mapping !== '') {
180
- data.model_mapping = JSON.stringify(
181
- JSON.parse(data.model_mapping),
182
- null,
183
- 2
184
- );
185
- }
186
- setInputs(data);
187
- if (data.auto_ban === 0) {
188
- setAutoBan(false);
189
- } else {
190
- setAutoBan(true);
191
- }
192
- setBasicModels(getChannelModels(data.type));
193
- // console.log(data);
194
- } else {
195
- showError(message);
196
- }
197
- setLoading(false);
198
- };
199
-
200
-
201
- const fetchUpstreamModelList = async (name) => {
202
- // if (inputs['type'] !== 1) {
203
- // showError(t('仅支持 OpenAI 接口格式'));
204
- // return;
205
- // }
206
- setLoading(true);
207
- const models = inputs['models'] || [];
208
- let err = false;
209
-
210
- if (isEdit) {
211
- // 如果是编辑模式,使用已有的channel id获取模型列表
212
- const res = await API.get('/api/channel/fetch_models/' + channelId);
213
- if (res.data && res.data?.success) {
214
- models.push(...res.data.data);
215
- } else {
216
- err = true;
217
- }
218
- } else {
219
- // 如果是新建模式,通过后端代理获取模型列表
220
- if (!inputs?.['key']) {
221
- showError(t('请填写密钥'));
222
- err = true;
223
- } else {
224
- try {
225
- const res = await API.post('/api/channel/fetch_models', {
226
- base_url: inputs['base_url'],
227
- type: inputs['type'],
228
- key: inputs['key']
229
- });
230
-
231
- if (res.data && res.data.success) {
232
- models.push(...res.data.data);
233
- } else {
234
- err = true;
235
- }
236
- } catch (error) {
237
- console.error('Error fetching models:', error);
238
- err = true;
239
- }
240
- }
241
- }
242
-
243
- if (!err) {
244
- handleInputChange(name, Array.from(new Set(models)));
245
- showSuccess(t('获取模型列表成功'));
246
- } else {
247
- showError(t('获取模型列表失败'));
248
- }
249
- setLoading(false);
250
- };
251
-
252
- const fetchModels = async () => {
253
- try {
254
- let res = await API.get(`/api/channel/models`);
255
- let localModelOptions = res.data.data.map((model) => ({
256
- label: model.id,
257
- value: model.id
258
- }));
259
- setOriginModelOptions(localModelOptions);
260
- setFullModels(res.data.data.map((model) => model.id));
261
- setBasicModels(
262
- res.data.data
263
- .filter((model) => {
264
- return model.id.startsWith('gpt-') || model.id.startsWith('text-');
265
- })
266
- .map((model) => model.id)
267
- );
268
- } catch (error) {
269
- showError(error.message);
270
- }
271
- };
272
-
273
- const fetchGroups = async () => {
274
- try {
275
- let res = await API.get(`/api/group/`);
276
- if (res === undefined) {
277
- return;
278
- }
279
- setGroupOptions(
280
- res.data.data.map((group) => ({
281
- label: group,
282
- value: group
283
- }))
284
- );
285
- } catch (error) {
286
- showError(error.message);
287
- }
288
- };
289
-
290
- useEffect(() => {
291
- let localModelOptions = [...originModelOptions];
292
- inputs.models.forEach((model) => {
293
- if (!localModelOptions.find((option) => option.label === model)) {
294
- localModelOptions.push({
295
- label: model,
296
- value: model
297
- });
298
- }
299
- });
300
- setModelOptions(localModelOptions);
301
- }, [originModelOptions, inputs.models]);
302
-
303
- useEffect(() => {
304
- fetchModels().then();
305
- fetchGroups().then();
306
- if (isEdit) {
307
- loadChannel().then(() => {});
308
- } else {
309
- setInputs(originInputs);
310
- let localModels = getChannelModels(inputs.type);
311
- setBasicModels(localModels);
312
- setInputs((inputs) => ({ ...inputs, models: localModels }));
313
- }
314
- }, [props.editingChannel.id]);
315
-
316
- const submit = async () => {
317
- if (!isEdit && (inputs.name === '' || inputs.key === '')) {
318
- showInfo(t('请填写渠道名称和渠道密钥!'));
319
- return;
320
- }
321
- if (inputs.models.length === 0) {
322
- showInfo(t('请至少选择一个模型!'));
323
- return;
324
- }
325
- if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
326
- showInfo(t('模型映射必须是合法的 JSON 格式!'));
327
- return;
328
- }
329
- let localInputs = { ...inputs };
330
- if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
331
- localInputs.base_url = localInputs.base_url.slice(
332
- 0,
333
- localInputs.base_url.length - 1
334
- );
335
- }
336
- if (localInputs.type === 18 && localInputs.other === '') {
337
- localInputs.other = 'v2.1';
338
- }
339
- let res;
340
- if (!Array.isArray(localInputs.models)) {
341
- showError(t('提交失败,请勿重复提交!'));
342
- handleCancel();
343
- return;
344
- }
345
- localInputs.auto_ban = autoBan ? 1 : 0;
346
- localInputs.models = localInputs.models.join(',');
347
- localInputs.group = localInputs.groups.join(',');
348
- if (isEdit) {
349
- res = await API.put(`/api/channel/`, {
350
- ...localInputs,
351
- id: parseInt(channelId)
352
- });
353
- } else {
354
- res = await API.post(`/api/channel/`, localInputs);
355
- }
356
- const { success, message } = res.data;
357
- if (success) {
358
- if (isEdit) {
359
- showSuccess(t('渠道更新成功!'));
360
- } else {
361
- showSuccess(t('渠道创建成功!'));
362
- setInputs(originInputs);
363
- }
364
- props.refresh();
365
- props.handleClose();
366
- } else {
367
- showError(message);
368
- }
369
- };
370
-
371
- const addCustomModels = () => {
372
- if (customModel.trim() === '') return;
373
- const modelArray = customModel.split(',').map((model) => model.trim());
374
-
375
- let localModels = [...inputs.models];
376
- let localModelOptions = [...modelOptions];
377
- let hasError = false;
378
-
379
- modelArray.forEach((model) => {
380
- if (model && !localModels.includes(model)) {
381
- localModels.push(model);
382
- localModelOptions.push({
383
- key: model,
384
- text: model,
385
- value: model
386
- });
387
- } else if (model) {
388
- showError(t('某些模型已存在!'));
389
- hasError = true;
390
- }
391
- });
392
-
393
- if (hasError) return;
394
-
395
- setModelOptions(localModelOptions);
396
- setCustomModel('');
397
- handleInputChange('models', localModels);
398
- };
399
-
400
-
401
- return (
402
- <>
403
- <SideSheet
404
- maskClosable={false}
405
- placement={isEdit ? 'right' : 'left'}
406
- title={
407
- <Title level={3}>{isEdit ? t('更新渠道信息') : t('创建新的渠道')}</Title>
408
- }
409
- headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
410
- bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
411
- visible={props.visible}
412
- footer={
413
- <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
414
- <Space>
415
- <Button theme="solid" size={'large'} onClick={submit}>
416
- {t('提交')}
417
- </Button>
418
- <Button
419
- theme="solid"
420
- size={'large'}
421
- type={'tertiary'}
422
- onClick={handleCancel}
423
- >
424
- {t('取消')}
425
- </Button>
426
- </Space>
427
- </div>
428
- }
429
- closeIcon={null}
430
- onCancel={() => handleCancel()}
431
- width={isMobile() ? '100%' : 600}
432
- >
433
- <Spin spinning={loading}>
434
- <div style={{ marginTop: 10 }}>
435
-
436
- <Typography.Text strong>{t('类型')}:</Typography.Text>
437
- </div>
438
- <Select
439
- name="type"
440
- required
441
- optionList={CHANNEL_OPTIONS}
442
- value={inputs.type}
443
- onChange={(value) => handleInputChange('type', value)}
444
- style={{ width: '50%' }}
445
- filter
446
- searchPosition='dropdown'
447
- placeholder={t('请选择渠道类型')}
448
- />
449
- {inputs.type === 40 && (
450
- <div style={{ marginTop: 10 }}>
451
- <Banner
452
- type="info"
453
- description={
454
- <div>
455
- <Typography.Text strong>
456
- {t('邀请链接')}:
457
- </Typography.Text>
458
- <Typography.Text
459
- link
460
- underline
461
- style={{marginLeft: 8}}
462
- onClick={() => window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')}
463
- >
464
- https://cloud.siliconflow.cn/i/hij0YNTZ
465
- </Typography.Text>
466
- </div>
467
- }
468
- />
469
- </div>
470
- )}
471
- {inputs.type === 3 && (
472
- <>
473
- <div style={{ marginTop: 10 }}>
474
- <Banner
475
- type={'warning'}
476
- description={t('注意,模型部署名称必须和模型名称保持一致')}
477
- ></Banner>
478
- </div>
479
- <div style={{ marginTop: 10 }}>
480
- <Typography.Text strong>
481
- AZURE_OPENAI_ENDPOINT:
482
- </Typography.Text>
483
- </div>
484
- <Input
485
- label="AZURE_OPENAI_ENDPOINT"
486
- name="azure_base_url"
487
- placeholder={t('请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com')}
488
- onChange={(value) => {
489
- handleInputChange('base_url', value);
490
- }}
491
- value={inputs.base_url}
492
- autoComplete="new-password"
493
- />
494
- <div style={{ marginTop: 10 }}>
495
- <Typography.Text strong>{t('默认 API 版���')}:</Typography.Text>
496
- </div>
497
- <Input
498
- label={t('默认 API 版本')}
499
- name="azure_other"
500
- placeholder={t('请输入默认 API 版本,例如:2024-12-01-preview')}
501
- onChange={(value) => {
502
- handleInputChange('other', value);
503
- }}
504
- value={inputs.other}
505
- autoComplete="new-password"
506
- />
507
- </>
508
- )}
509
- {inputs.type === 8 && (
510
- <>
511
- <div style={{ marginTop: 10 }}>
512
- <Banner
513
- type={'warning'}
514
- description={t('如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。')}
515
- ></Banner>
516
- </div>
517
- <div style={{ marginTop: 10 }}>
518
- <Typography.Text strong>
519
- {t('完整的 Base URL,支持变量{model}')}:
520
- </Typography.Text>
521
- </div>
522
- <Input
523
- name="base_url"
524
- placeholder={t('请输入完整的URL,例如:https://api.openai.com/v1/chat/completions')}
525
- onChange={(value) => {
526
- handleInputChange('base_url', value);
527
- }}
528
- value={inputs.base_url}
529
- autoComplete="new-password"
530
- />
531
- </>
532
- )}
533
- {inputs.type === 37 && (
534
- <>
535
- <div style={{ marginTop: 10 }}>
536
- <Banner
537
- type={'warning'}
538
- description={t('Dify渠道只适配chatflow和agent,并且agent不支持图片!')}
539
- ></Banner>
540
- </div>
541
- </>
542
- )}
543
- <div style={{ marginTop: 10 }}>
544
- <Typography.Text strong>{t('名称')}:</Typography.Text>
545
- </div>
546
- <Input
547
- required
548
- name="name"
549
- placeholder={t('请为渠道命名')}
550
- onChange={(value) => {
551
- handleInputChange('name', value);
552
- }}
553
- value={inputs.name}
554
- autoComplete="new-password"
555
- />
556
- {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && (
557
- <>
558
- <div style={{ marginTop: 10 }}>
559
- <Typography.Text strong>{t('代理站地址')}:</Typography.Text>
560
- </div>
561
- <Tooltip content={t('对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写')}>
562
- <Input
563
- label={t('代理站地址')}
564
- name="base_url"
565
- placeholder={t('此项可选,用于通过代理站来进行 API 调用,末尾不要带/v1和/')}
566
- onChange={(value) => {
567
- handleInputChange('base_url', value);
568
- }}
569
- value={inputs.base_url}
570
- autoComplete="new-password"
571
- />
572
- </Tooltip>
573
- </>
574
- )}
575
- <div style={{ marginTop: 10 }}>
576
- <Typography.Text strong>{t('密钥')}:</Typography.Text>
577
- </div>
578
- {batch ? (
579
- <TextArea
580
- label={t('密钥')}
581
- name="key"
582
- required
583
- placeholder={t('请输入密钥,一行一个')}
584
- onChange={(value) => {
585
- handleInputChange('key', value);
586
- }}
587
- value={inputs.key}
588
- style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
589
- autoComplete="new-password"
590
- />
591
- ) : (
592
- <>
593
- {inputs.type === 41 ? (
594
- <TextArea
595
- label={t('鉴权json')}
596
- name="key"
597
- required
598
- placeholder={'{\n' +
599
- ' "type": "service_account",\n' +
600
- ' "project_id": "abc-bcd-123-456",\n' +
601
- ' "private_key_id": "123xxxxx456",\n' +
602
- ' "private_key": "-----BEGIN PRIVATE KEY-----xxxx\n' +
603
- ' "client_email": "xxx@developer.gserviceaccount.com",\n' +
604
- ' "client_id": "111222333",\n' +
605
- ' "auth_uri": "https://accounts.google.com/o/oauth2/auth",\n' +
606
- ' "token_uri": "https://oauth2.googleapis.com/token",\n' +
607
- ' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' +
608
- ' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' +
609
- ' "universe_domain": "googleapis.com"\n' +
610
- '}'}
611
- onChange={(value) => {
612
- handleInputChange('key', value);
613
- }}
614
- autosize={{ minRows: 10 }}
615
- value={inputs.key}
616
- autoComplete="new-password"
617
- />
618
- ) : (
619
- <Input
620
- label={t('密钥')}
621
- name="key"
622
- required
623
- placeholder={t(type2secretPrompt(inputs.type))}
624
- onChange={(value) => {
625
- handleInputChange('key', value);
626
- }}
627
- value={inputs.key}
628
- autoComplete="new-password"
629
- />
630
- )}
631
- </>
632
- )}
633
- {!isEdit && (
634
- <div style={{ marginTop: 10, display: 'flex' }}>
635
- <Space>
636
- <Checkbox
637
- checked={batch}
638
- label={t('批量创建')}
639
- name="batch"
640
- onChange={() => setBatch(!batch)}
641
- />
642
- <Typography.Text strong>{t('批量创建')}</Typography.Text>
643
- </Space>
644
- </div>
645
- )}
646
- {inputs.type === 22 && (
647
- <>
648
- <div style={{ marginTop: 10 }}>
649
- <Typography.Text strong>{t('私有部署地址')}:</Typography.Text>
650
- </div>
651
- <Input
652
- name="base_url"
653
- placeholder={t('请输入私有部署地址,格式为:https://fastgpt.run/api/openapi')}
654
- onChange={(value) => {
655
- handleInputChange('base_url', value);
656
- }}
657
- value={inputs.base_url}
658
- autoComplete="new-password"
659
- />
660
- </>
661
- )}
662
- {inputs.type === 36 && (
663
- <>
664
- <div style={{ marginTop: 10 }}>
665
- <Typography.Text strong>
666
- {t('注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用')}
667
- </Typography.Text>
668
- </div>
669
- <Input
670
- name="base_url"
671
- placeholder={t('请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com')}
672
- onChange={(value) => {
673
- handleInputChange('base_url', value);
674
- }}
675
- value={inputs.base_url}
676
- autoComplete="new-password"
677
- />
678
- </>
679
- )}
680
- <div style={{ marginTop: 10 }}>
681
- <Typography.Text strong>{t('分组')}:</Typography.Text>
682
- </div>
683
- <Select
684
- placeholder={t('请选择可以使用该渠道的分组')}
685
- name="groups"
686
- required
687
- multiple
688
- selection
689
- allowAdditions
690
- additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
691
- onChange={(value) => {
692
- handleInputChange('groups', value);
693
- }}
694
- value={inputs.groups}
695
- autoComplete="new-password"
696
- optionList={groupOptions}
697
- />
698
- {inputs.type === 18 && (
699
- <>
700
- <div style={{ marginTop: 10 }}>
701
- <Typography.Text strong>模型版本:</Typography.Text>
702
- </div>
703
- <Input
704
- name="other"
705
- placeholder={
706
- '请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
707
- }
708
- onChange={(value) => {
709
- handleInputChange('other', value);
710
- }}
711
- value={inputs.other}
712
- autoComplete="new-password"
713
- />
714
- </>
715
- )}
716
- {inputs.type === 41 && (
717
- <>
718
- <div style={{ marginTop: 10 }}>
719
- <Typography.Text strong>{t('部署地区')}:</Typography.Text>
720
- </div>
721
- <TextArea
722
- name="other"
723
- placeholder={t('请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
724
- '{\n' +
725
- ' "default": "us-central1",\n' +
726
- ' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
727
- '}')}
728
- autosize={{ minRows: 2 }}
729
- onChange={(value) => {
730
- handleInputChange('other', value);
731
- }}
732
- value={inputs.other}
733
- autoComplete="new-password"
734
- />
735
- <Typography.Text
736
- style={{
737
- color: 'rgba(var(--semi-blue-5), 1)',
738
- userSelect: 'none',
739
- cursor: 'pointer'
740
- }}
741
- onClick={() => {
742
- handleInputChange(
743
- 'other',
744
- JSON.stringify(REGION_EXAMPLE, null, 2)
745
- );
746
- }}
747
- >
748
- {t('填入模板')}
749
- </Typography.Text>
750
- </>
751
- )}
752
- {inputs.type === 21 && (
753
- <>
754
- <div style={{ marginTop: 10 }}>
755
- <Typography.Text strong>知识库 ID:</Typography.Text>
756
- </div>
757
- <Input
758
- label="知识库 ID"
759
- name="other"
760
- placeholder={'请输入知识库 ID,例如:123456'}
761
- onChange={(value) => {
762
- handleInputChange('other', value);
763
- }}
764
- value={inputs.other}
765
- autoComplete="new-password"
766
- />
767
- </>
768
- )}
769
- {inputs.type === 39 && (
770
- <>
771
- <div style={{ marginTop: 10 }}>
772
- <Typography.Text strong>Account ID:</Typography.Text>
773
- </div>
774
- <Input
775
- name="other"
776
- placeholder={
777
- '请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'
778
- }
779
- onChange={(value) => {
780
- handleInputChange('other', value);
781
- }}
782
- value={inputs.other}
783
- autoComplete="new-password"
784
- />
785
- </>
786
- )}
787
- <div style={{ marginTop: 10 }}>
788
- <Typography.Text strong>{t('模型')}:</Typography.Text>
789
- </div>
790
- <Select
791
- placeholder={'请选择该渠道所支持的模型'}
792
- name="models"
793
- required
794
- multiple
795
- selection
796
- filter
797
- searchPosition='dropdown'
798
- onChange={(value) => {
799
- handleInputChange('models', value);
800
- }}
801
- value={inputs.models}
802
- autoComplete="new-password"
803
- optionList={modelOptions}
804
- />
805
- <div style={{ lineHeight: '40px', marginBottom: '12px' }}>
806
- <Space>
807
- <Button
808
- type="primary"
809
- onClick={() => {
810
- handleInputChange('models', basicModels);
811
- }}
812
- >
813
- {t('填入相关模型')}
814
- </Button>
815
- <Button
816
- type="secondary"
817
- onClick={() => {
818
- handleInputChange('models', fullModels);
819
- }}
820
- >
821
- {t('填入所有模型')}
822
- </Button>
823
- <Tooltip content={t('新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出')}>
824
- <Button
825
- type="tertiary"
826
- onClick={() => {
827
- fetchUpstreamModelList('models');
828
- }}
829
- >
830
- {t('获取模型列表')}
831
- </Button>
832
- </Tooltip>
833
- <Button
834
- type="warning"
835
- onClick={() => {
836
- handleInputChange('models', []);
837
- }}
838
- >
839
- {t('清除所有模型')}
840
- </Button>
841
- </Space>
842
- <Input
843
- addonAfter={
844
- <Button type="primary" onClick={addCustomModels}>
845
- {t('填入')}
846
- </Button>
847
- }
848
- placeholder={t('输入自定义模型名称')}
849
- value={customModel}
850
- onChange={(value) => {
851
- setCustomModel(value.trim());
852
- }}
853
- />
854
- </div>
855
- <div style={{ marginTop: 10 }}>
856
- <Typography.Text strong>{t('模型重定向')}:</Typography.Text>
857
- </div>
858
- <TextArea
859
- placeholder={t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') + `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
860
- name="model_mapping"
861
- onChange={(value) => {
862
- handleInputChange('model_mapping', value);
863
- }}
864
- autosize
865
- value={inputs.model_mapping}
866
- autoComplete="new-password"
867
- />
868
- <Typography.Text
869
- style={{
870
- color: 'rgba(var(--semi-blue-5), 1)',
871
- userSelect: 'none',
872
- cursor: 'pointer'
873
- }}
874
- onClick={() => {
875
- handleInputChange(
876
- 'model_mapping',
877
- JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
878
- );
879
- }}
880
- >
881
- {t('入模')}
882
- </Typography.Text>
883
- <div style={{ marginTop: 10 }}>
884
- <Typography.Text strong>
885
- {t('渠道标签')}
886
- </Typography.Text>
887
- </div>
888
- <Input
889
- label={t('渠道标签')}
890
- name="tag"
891
- placeholder={t('渠道标签')}
892
- onChange={(value) => {
893
- handleInputChange('tag', value);
894
- }}
895
- value={inputs.tag}
896
- autoComplete="new-password"
897
- />
898
- <div style={{ marginTop: 10 }}>
899
- <Typography.Text strong>
900
- {t('渠道优先级')}
901
- </Typography.Text>
902
- </div>
903
- <Input
904
- label={t('渠道优先级')}
905
- name="priority"
906
- placeholder={t('渠道优先级')}
907
- onChange={(value) => {
908
- const number = parseInt(value);
909
- if (isNaN(number)) {
910
- handleInputChange('priority', value);
911
- } else {
912
- handleInputChange('priority', number);
913
- }
914
- }}
915
- value={inputs.priority}
916
- autoComplete="new-password"
917
- />
918
- <div style={{ marginTop: 10 }}>
919
- <Typography.Text strong>
920
- {t('渠道权重')}
921
- </Typography.Text>
922
- </div>
923
- <Input
924
- label={t('渠道权重')}
925
- name="weight"
926
- placeholder={t('渠道权重')}
927
- onChange={(value) => {
928
- const number = parseInt(value);
929
- if (isNaN(number)) {
930
- handleInputChange('weight', value);
931
- } else {
932
- handleInputChange('weight', number);
933
- }
934
- }}
935
- value={inputs.weight}
936
- autoComplete="new-password"
937
- />
938
- <>
939
- <div style={{ marginTop: 10 }}>
940
- <Typography.Text strong>
941
- {t('渠道额外设置')}:
942
- </Typography.Text>
943
- </div>
944
- <TextArea
945
- placeholder={t('此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:') + '\n{\n "force_format": true\n}'}
946
- name="setting"
947
- onChange={(value) => {
948
- handleInputChange('setting', value);
949
- }}
950
- autosize
951
- value={inputs.setting}
952
- autoComplete="new-password"
953
- />
954
- <Space>
955
- <Typography.Text
956
- style={{
957
- color: 'rgba(var(--semi-blue-5), 1)',
958
- userSelect: 'none',
959
- cursor: 'pointer'
960
- }}
961
- onClick={() => {
962
- handleInputChange(
963
- 'setting',
964
- JSON.stringify({
965
- force_format: true
966
- }, null, 2)
967
- );
968
- }}
969
- >
970
- {t('填入模板')}
971
- </Typography.Text>
972
- <Typography.Text
973
- style={{
974
- color: 'rgba(var(--semi-blue-5), 1)',
975
- userSelect: 'none',
976
- cursor: 'pointer'
977
- }}
978
- onClick={() => {
979
- window.open('https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md');
980
- }}
981
- >
982
- {t('设置说明')}
983
- </Typography.Text>
984
- </Space>
985
- </>
986
- <>
987
- <div style={{ marginTop: 10 }}>
988
- <Typography.Text strong>
989
- {t('参数覆盖')}:
990
- </Typography.Text>
991
- </div>
992
- <TextArea
993
- placeholder={t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:') + '\n{\n "temperature": 0\n}'}
994
- name="setting"
995
- onChange={(value) => {
996
- handleInputChange('param_override', value);
997
- }}
998
- autosize
999
- value={inputs.param_override}
1000
- autoComplete="new-password"
1001
- />
1002
- </>
1003
- {inputs.type === 1 && (
1004
- <>
1005
- <div style={{ marginTop: 10 }}>
1006
- <Typography.Text strong>{t('组织')}:</Typography.Text>
1007
- </div>
1008
- <Input
1009
- label={t('组织,可选,不填则为默认组织')}
1010
- name="openai_organization"
1011
- placeholder={t('请输入组织org-xxx')}
1012
- onChange={(value) => {
1013
- handleInputChange('openai_organization', value);
1014
- }}
1015
- value={inputs.openai_organization}
1016
- />
1017
- </>
1018
- )}
1019
- <div style={{ marginTop: 10 }}>
1020
- <Typography.Text strong>{t('默认测试模型')}:</Typography.Text>
1021
- </div>
1022
- <Input
1023
- name="test_model"
1024
- placeholder={t('不填则为模型列表第一个')}
1025
- onChange={(value) => {
1026
- handleInputChange('test_model', value);
1027
- }}
1028
- value={inputs.test_model}
1029
- />
1030
- <div style={{ marginTop: 10, display: 'flex' }}>
1031
- <Space>
1032
- <Checkbox
1033
- name="auto_ban"
1034
- checked={autoBan}
1035
- onChange={() => {
1036
- setAutoBan(!autoBan);
1037
- }}
1038
- />
1039
- <Typography.Text strong>
1040
- {t('是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:')}
1041
- </Typography.Text>
1042
- </Space>
1043
- </div>
1044
- <div style={{ marginTop: 10 }}>
1045
- <Typography.Text strong>
1046
- {t('状态码复写(仅影响本地判断,不修改返回到上游的状态码)')}:
1047
- </Typography.Text>
1048
- </div>
1049
- <TextArea
1050
- placeholder={t('此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:') +
1051
- '\n' + JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}
1052
- name="status_code_mapping"
1053
- onChange={(value) => {
1054
- handleInputChange('status_code_mapping', value);
1055
- }}
1056
- autosize
1057
- value={inputs.status_code_mapping}
1058
- autoComplete="new-password"
1059
- />
1060
- <Typography.Text
1061
- style={{
1062
- color: 'rgba(var(--semi-blue-5), 1)',
1063
- userSelect: 'none',
1064
- cursor: 'pointer'
1065
- }}
1066
- onClick={() => {
1067
- handleInputChange(
1068
- 'status_code_mapping',
1069
- JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
1070
- );
1071
- }}
1072
- >
1073
- {t('填入模板')}
1074
- </Typography.Text>
1075
- </Spin>
1076
- </SideSheet>
1077
- </>
1078
- );
1079
- };
1080
-
1081
- export default EditChannel;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { useNavigate, useParams } from 'react-router-dom';
3
+ import { useTranslation } from 'react-i18next';
4
+ import {
5
+ API,
6
+ isMobile,
7
+ showError,
8
+ showInfo,
9
+ showSuccess, showWarning,
10
+ verifyJSON
11
+ } from '../../helpers';
12
+ import { CHANNEL_OPTIONS } from '../../constants';
13
+ import Title from '@douyinfe/semi-ui/lib/es/typography/title';
14
+ import {
15
+ SideSheet,
16
+ Space,
17
+ Spin,
18
+ Button,
19
+ Tooltip,
20
+ Input,
21
+ Typography,
22
+ Select,
23
+ TextArea,
24
+ Checkbox,
25
+ Banner, Modal
26
+ } from '@douyinfe/semi-ui';
27
+ import { getChannelModels, loadChannelModels } from '../../components/utils.js';
28
+
29
+ const MODEL_MAPPING_EXAMPLE = {
30
+ 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
31
+ };
32
+
33
+ const STATUS_CODE_MAPPING_EXAMPLE = {
34
+ 400: '500'
35
+ };
36
+
37
+ const REGION_EXAMPLE = {
38
+ 'default': 'us-central1',
39
+ 'claude-3-5-sonnet-20240620': 'europe-west1'
40
+ };
41
+
42
+ function type2secretPrompt(type) {
43
+ // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
44
+ switch (type) {
45
+ case 15:
46
+ return '按照如下格式输入:APIKey|SecretKey';
47
+ case 18:
48
+ return '按照如下格式输入:APPID|APISecret|APIKey';
49
+ case 22:
50
+ return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
51
+ case 23:
52
+ return '按照如下格式输入:AppId|SecretId|SecretKey';
53
+ case 33:
54
+ return '按照如下格式输入:Ak|Sk|Region';
55
+ default:
56
+ return '请输入渠道对应的鉴权密钥';
57
+ }
58
+ }
59
+
60
+ const EditChannel = (props) => {
61
+ const { t } = useTranslation();
62
+ const navigate = useNavigate();
63
+ const channelId = props.editingChannel.id;
64
+ const isEdit = channelId !== undefined;
65
+ const [loading, setLoading] = useState(isEdit);
66
+ const handleCancel = () => {
67
+ props.handleClose();
68
+ };
69
+ const originInputs = {
70
+ name: '',
71
+ type: 1,
72
+ key: '',
73
+ openai_organization: '',
74
+ max_input_tokens: 0,
75
+ base_url: '',
76
+ other: '',
77
+ model_mapping: '',
78
+ status_code_mapping: '',
79
+ models: [],
80
+ auto_ban: 1,
81
+ test_model: '',
82
+ groups: ['default'],
83
+ priority: 0,
84
+ weight: 0,
85
+ tag: ''
86
+ };
87
+ const [batch, setBatch] = useState(false);
88
+ const [autoBan, setAutoBan] = useState(true);
89
+ // const [autoBan, setAutoBan] = useState(true);
90
+ const [inputs, setInputs] = useState(originInputs);
91
+ const [originModelOptions, setOriginModelOptions] = useState([]);
92
+ const [modelOptions, setModelOptions] = useState([]);
93
+ const [groupOptions, setGroupOptions] = useState([]);
94
+ const [basicModels, setBasicModels] = useState([]);
95
+ const [fullModels, setFullModels] = useState([]);
96
+ const [customModel, setCustomModel] = useState('');
97
+ const handleInputChange = (name, value) => {
98
+ if (name === 'base_url' && value.endsWith('/v1')) {
99
+ Modal.confirm({
100
+ title: '警告',
101
+ content: '不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
102
+ onOk: () => {
103
+ setInputs((inputs) => ({ ...inputs, [name]: value }));
104
+ }
105
+ })
106
+ return
107
+ }
108
+ setInputs((inputs) => ({ ...inputs, [name]: value }));
109
+ if (name === 'type') {
110
+ let localModels = [];
111
+ switch (value) {
112
+ case 2:
113
+ localModels = [
114
+ 'mj_imagine',
115
+ 'mj_variation',
116
+ 'mj_reroll',
117
+ 'mj_blend',
118
+ 'mj_upscale',
119
+ 'mj_describe',
120
+ 'mj_uploads'
121
+ ];
122
+ break;
123
+ case 5:
124
+ localModels = [
125
+ 'swap_face',
126
+ 'mj_imagine',
127
+ 'mj_variation',
128
+ 'mj_reroll',
129
+ 'mj_blend',
130
+ 'mj_upscale',
131
+ 'mj_describe',
132
+ 'mj_zoom',
133
+ 'mj_shorten',
134
+ 'mj_modal',
135
+ 'mj_inpaint',
136
+ 'mj_custom_zoom',
137
+ 'mj_high_variation',
138
+ 'mj_low_variation',
139
+ 'mj_pan',
140
+ 'mj_uploads'
141
+ ];
142
+ break;
143
+ case 36:
144
+ localModels = [
145
+ 'suno_music',
146
+ 'suno_lyrics'
147
+ ];
148
+ break;
149
+ default:
150
+ localModels = getChannelModels(value);
151
+ break;
152
+ }
153
+ if (inputs.models.length === 0) {
154
+ setInputs((inputs) => ({ ...inputs, models: localModels }));
155
+ }
156
+ setBasicModels(localModels);
157
+ }
158
+ //setAutoBan
159
+ };
160
+
161
+ const loadChannel = async () => {
162
+ setLoading(true);
163
+ let res = await API.get(`/api/channel/${channelId}`);
164
+ if (res === undefined) {
165
+ return;
166
+ }
167
+ const { success, message, data } = res.data;
168
+ if (success) {
169
+ if (data.models === '') {
170
+ data.models = [];
171
+ } else {
172
+ data.models = data.models.split(',');
173
+ }
174
+ if (data.group === '') {
175
+ data.groups = [];
176
+ } else {
177
+ data.groups = data.group.split(',');
178
+ }
179
+ if (data.model_mapping !== '') {
180
+ data.model_mapping = JSON.stringify(
181
+ JSON.parse(data.model_mapping),
182
+ null,
183
+ 2
184
+ );
185
+ }
186
+ setInputs(data);
187
+ if (data.auto_ban === 0) {
188
+ setAutoBan(false);
189
+ } else {
190
+ setAutoBan(true);
191
+ }
192
+ setBasicModels(getChannelModels(data.type));
193
+ // console.log(data);
194
+ } else {
195
+ showError(message);
196
+ }
197
+ setLoading(false);
198
+ };
199
+
200
+
201
+ const fetchUpstreamModelList = async (name) => {
202
+ // if (inputs['type'] !== 1) {
203
+ // showError(t('仅支持 OpenAI 接口格式'));
204
+ // return;
205
+ // }
206
+ setLoading(true);
207
+ const models = inputs['models'] || [];
208
+ let err = false;
209
+
210
+ if (isEdit) {
211
+ // 如果是编辑模式,使用已有的channel id获取模型列表
212
+ const res = await API.get('/api/channel/fetch_models/' + channelId);
213
+ if (res.data && res.data?.success) {
214
+ models.push(...res.data.data);
215
+ } else {
216
+ err = true;
217
+ }
218
+ } else {
219
+ // 如果是新建模式,通过后端代理获取模型列表
220
+ if (!inputs?.['key']) {
221
+ showError(t('请填写密钥'));
222
+ err = true;
223
+ } else {
224
+ try {
225
+ const res = await API.post('/api/channel/fetch_models', {
226
+ base_url: inputs['base_url'],
227
+ type: inputs['type'],
228
+ key: inputs['key']
229
+ });
230
+
231
+ if (res.data && res.data.success) {
232
+ models.push(...res.data.data);
233
+ } else {
234
+ err = true;
235
+ }
236
+ } catch (error) {
237
+ console.error('Error fetching models:', error);
238
+ err = true;
239
+ }
240
+ }
241
+ }
242
+
243
+ if (!err) {
244
+ handleInputChange(name, Array.from(new Set(models)));
245
+ showSuccess(t('获取模型列表成功'));
246
+ } else {
247
+ showError(t('获取模型列表失败'));
248
+ }
249
+ setLoading(false);
250
+ };
251
+
252
+ const fetchModels = async () => {
253
+ try {
254
+ let res = await API.get(`/api/channel/models`);
255
+ let localModelOptions = res.data.data.map((model) => ({
256
+ label: model.id,
257
+ value: model.id
258
+ }));
259
+ setOriginModelOptions(localModelOptions);
260
+ setFullModels(res.data.data.map((model) => model.id));
261
+ setBasicModels(
262
+ res.data.data
263
+ .filter((model) => {
264
+ return model.id.startsWith('gpt-') || model.id.startsWith('text-');
265
+ })
266
+ .map((model) => model.id)
267
+ );
268
+ } catch (error) {
269
+ showError(error.message);
270
+ }
271
+ };
272
+
273
+ const fetchGroups = async () => {
274
+ try {
275
+ let res = await API.get(`/api/group/`);
276
+ if (res === undefined) {
277
+ return;
278
+ }
279
+ setGroupOptions(
280
+ res.data.data.map((group) => ({
281
+ label: group,
282
+ value: group
283
+ }))
284
+ );
285
+ } catch (error) {
286
+ showError(error.message);
287
+ }
288
+ };
289
+
290
+ useEffect(() => {
291
+ let localModelOptions = [...originModelOptions];
292
+ inputs.models.forEach((model) => {
293
+ if (!localModelOptions.find((option) => option.label === model)) {
294
+ localModelOptions.push({
295
+ label: model,
296
+ value: model
297
+ });
298
+ }
299
+ });
300
+ setModelOptions(localModelOptions);
301
+ }, [originModelOptions, inputs.models]);
302
+
303
+ useEffect(() => {
304
+ fetchModels().then();
305
+ fetchGroups().then();
306
+ if (isEdit) {
307
+ loadChannel().then(() => {});
308
+ } else {
309
+ setInputs(originInputs);
310
+ let localModels = getChannelModels(inputs.type);
311
+ setBasicModels(localModels);
312
+ setInputs((inputs) => ({ ...inputs, models: localModels }));
313
+ }
314
+ }, [props.editingChannel.id]);
315
+
316
+ const submit = async () => {
317
+ if (!isEdit && (inputs.name === '' || inputs.key === '')) {
318
+ showInfo(t('请填写渠道名称和渠道密钥!'));
319
+ return;
320
+ }
321
+ if (inputs.models.length === 0) {
322
+ showInfo(t('请至少选择一个模型!'));
323
+ return;
324
+ }
325
+ if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
326
+ showInfo(t('模型映射必须是合法的 JSON 格式!'));
327
+ return;
328
+ }
329
+ let localInputs = { ...inputs };
330
+ if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
331
+ localInputs.base_url = localInputs.base_url.slice(
332
+ 0,
333
+ localInputs.base_url.length - 1
334
+ );
335
+ }
336
+ if (localInputs.type === 18 && localInputs.other === '') {
337
+ localInputs.other = 'v2.1';
338
+ }
339
+ let res;
340
+ if (!Array.isArray(localInputs.models)) {
341
+ showError(t('提交失败,请勿重复提交!'));
342
+ handleCancel();
343
+ return;
344
+ }
345
+ localInputs.auto_ban = autoBan ? 1 : 0;
346
+ localInputs.models = localInputs.models.join(',');
347
+ localInputs.group = localInputs.groups.join(',');
348
+ if (isEdit) {
349
+ res = await API.put(`/api/channel/`, {
350
+ ...localInputs,
351
+ id: parseInt(channelId)
352
+ });
353
+ } else {
354
+ res = await API.post(`/api/channel/`, localInputs);
355
+ }
356
+ const { success, message } = res.data;
357
+ if (success) {
358
+ if (isEdit) {
359
+ showSuccess(t('渠道更新成功!'));
360
+ } else {
361
+ showSuccess(t('渠道创建成功!'));
362
+ setInputs(originInputs);
363
+ }
364
+ props.refresh();
365
+ props.handleClose();
366
+ } else {
367
+ showError(message);
368
+ }
369
+ };
370
+
371
+ const addCustomModels = () => {
372
+ if (customModel.trim() === '') return;
373
+ const modelArray = customModel.split(',').map((model) => model.trim());
374
+
375
+ let localModels = [...inputs.models];
376
+ let localModelOptions = [...modelOptions];
377
+ let hasError = false;
378
+
379
+ modelArray.forEach((model) => {
380
+ if (model && !localModels.includes(model)) {
381
+ localModels.push(model);
382
+ localModelOptions.push({
383
+ key: model,
384
+ text: model,
385
+ value: model
386
+ });
387
+ } else if (model) {
388
+ showError(t('某些模型已存在!'));
389
+ hasError = true;
390
+ }
391
+ });
392
+
393
+ if (hasError) return;
394
+
395
+ setModelOptions(localModelOptions);
396
+ setCustomModel('');
397
+ handleInputChange('models', localModels);
398
+ };
399
+
400
+ const handleAddPrefix = () => {
401
+ const channelName = inputs.name;
402
+ const currentModels = inputs.models;
403
+
404
+ if (!channelName || !currentModels || currentModels.length === 0) {
405
+ showError(t('请先填写渠道名称并选择模型!'));
406
+ return;
407
+ }
408
+
409
+ // 生成带前缀的模型列表
410
+ const prefixedModels = currentModels.map(model => `${channelName}/${model}`);
411
+
412
+ // 生成模型映射
413
+ const mapping = {};
414
+ prefixedModels.forEach((prefixedModel, index) => {
415
+ mapping[prefixedModel] = currentModels[index];
416
+ });
417
+
418
+ const modelMappingJson = JSON.stringify(mapping, null, 2);
419
+
420
+ // 更新状态
421
+ handleInputChange('models', prefixedModels);
422
+ handleInputChange('model_mapping', modelMappingJson);
423
+
424
+ showSuccess(t('前缀添加成功!'));
425
+ };
426
+
427
+ return (
428
+ <>
429
+ <SideSheet
430
+ maskClosable={false}
431
+ placement={isEdit ? 'right' : 'left'}
432
+ title={
433
+ <Title level={3}>{isEdit ? t('更新渠道信息') : t('创建新的渠道')}</Title>
434
+ }
435
+ headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
436
+ bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
437
+ visible={props.visible}
438
+ footer={
439
+ <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
440
+ <Space>
441
+ <Button theme="solid" size={'large'} onClick={submit}>
442
+ {t('提交')}
443
+ </Button>
444
+ <Button
445
+ theme="solid"
446
+ size={'large'}
447
+ type={'tertiary'}
448
+ onClick={handleCancel}
449
+ >
450
+ {t('取消')}
451
+ </Button>
452
+ </Space>
453
+ </div>
454
+ }
455
+ closeIcon={null}
456
+ onCancel={() => handleCancel()}
457
+ width={isMobile() ? '100%' : 600}
458
+ >
459
+ <Spin spinning={loading}>
460
+ <div style={{ marginTop: 10 }}>
461
+
462
+ <Typography.Text strong>{t('类型')}:</Typography.Text>
463
+ </div>
464
+ <Select
465
+ name="type"
466
+ required
467
+ optionList={CHANNEL_OPTIONS}
468
+ value={inputs.type}
469
+ onChange={(value) => handleInputChange('type', value)}
470
+ style={{ width: '50%' }}
471
+ filter
472
+ searchPosition='dropdown'
473
+ placeholder={t('请选择渠道类型')}
474
+ />
475
+ {inputs.type === 40 && (
476
+ <div style={{ marginTop: 10 }}>
477
+ <Banner
478
+ type="info"
479
+ description={
480
+ <div>
481
+ <Typography.Text strong>
482
+ {t('邀请链接')}:
483
+ </Typography.Text>
484
+ <Typography.Text
485
+ link
486
+ underline
487
+ style={{marginLeft: 8}}
488
+ onClick={() => window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')}
489
+ >
490
+ https://cloud.siliconflow.cn/i/hij0YNTZ
491
+ </Typography.Text>
492
+ </div>
493
+ }
494
+ />
495
+ </div>
496
+ )}
497
+ {inputs.type === 3 && (
498
+ <>
499
+ <div style={{ marginTop: 10 }}>
500
+ <Banner
501
+ type={'warning'}
502
+ description={t('注意,模型部署名称必须和模型名称保持一致')}
503
+ ></Banner>
504
+ </div>
505
+ <div style={{ marginTop: 10 }}>
506
+ <Typography.Text strong>
507
+ AZURE_OPENAI_ENDPOINT:
508
+ </Typography.Text>
509
+ </div>
510
+ <Input
511
+ label="AZURE_OPENAI_ENDPOINT"
512
+ name="azure_base_url"
513
+ placeholder={t('请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com')}
514
+ onChange={(value) => {
515
+ handleInputChange('base_url', value);
516
+ }}
517
+ value={inputs.base_url}
518
+ autoComplete="new-password"
519
+ />
520
+ <div style={{ marginTop: 10 }}>
521
+ <Typography.Text strong>{t('默认 API 版本')}:</Typography.Text>
522
+ </div>
523
+ <Input
524
+ label={t('默认 API 版本')}
525
+ name="azure_other"
526
+ placeholder={t('请输入默认 API 版本,例如:2024-12-01-preview')}
527
+ onChange={(value) => {
528
+ handleInputChange('other', value);
529
+ }}
530
+ value={inputs.other}
531
+ autoComplete="new-password"
532
+ />
533
+ </>
534
+ )}
535
+ {inputs.type === 8 && (
536
+ <>
537
+ <div style={{ marginTop: 10 }}>
538
+ <Banner
539
+ type={'warning'}
540
+ description={t('如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。')}
541
+ ></Banner>
542
+ </div>
543
+ <div style={{ marginTop: 10 }}>
544
+ <Typography.Text strong>
545
+ {t('完整的 Base URL,支持变量{model}')}:
546
+ </Typography.Text>
547
+ </div>
548
+ <Input
549
+ name="base_url"
550
+ placeholder={t('请输入完整的URL,例如:https://api.openai.com/v1/chat/completions')}
551
+ onChange={(value) => {
552
+ handleInputChange('base_url', value);
553
+ }}
554
+ value={inputs.base_url}
555
+ autoComplete="new-password"
556
+ />
557
+ </>
558
+ )}
559
+ {inputs.type === 37 && (
560
+ <>
561
+ <div style={{ marginTop: 10 }}>
562
+ <Banner
563
+ type={'warning'}
564
+ description={t('Dify渠道只适配chatflow和agent,并且agent不支持图片!')}
565
+ ></Banner>
566
+ </div>
567
+ </>
568
+ )}
569
+ <div style={{ marginTop: 10 }}>
570
+ <Typography.Text strong>{t('名称')}:</Typography.Text>
571
+ </div>
572
+ <Input
573
+ required
574
+ name="name"
575
+ placeholder={t('请为渠道命名')}
576
+ onChange={(value) => {
577
+ handleInputChange('name', value);
578
+ }}
579
+ value={inputs.name}
580
+ autoComplete="new-password"
581
+ />
582
+ {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && (
583
+ <>
584
+ <div style={{ marginTop: 10 }}>
585
+ <Typography.Text strong>{t('代理站地址')}:</Typography.Text>
586
+ </div>
587
+ <Tooltip content={t('对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写')}>
588
+ <Input
589
+ label={t('代理站地址')}
590
+ name="base_url"
591
+ placeholder={t('此项可选,用于通过代理站来进行 API 调用,末尾不要带/v1和/')}
592
+ onChange={(value) => {
593
+ handleInputChange('base_url', value);
594
+ }}
595
+ value={inputs.base_url}
596
+ autoComplete="new-password"
597
+ />
598
+ </Tooltip>
599
+ </>
600
+ )}
601
+ <div style={{ marginTop: 10 }}>
602
+ <Typography.Text strong>{t('密钥')}:</Typography.Text>
603
+ </div>
604
+ {batch ? (
605
+ <TextArea
606
+ label={t('密钥')}
607
+ name="key"
608
+ required
609
+ placeholder={t('请输入密钥,一行一个')}
610
+ onChange={(value) => {
611
+ handleInputChange('key', value);
612
+ }}
613
+ value={inputs.key}
614
+ style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
615
+ autoComplete="new-password"
616
+ />
617
+ ) : (
618
+ <>
619
+ {inputs.type === 41 ? (
620
+ <TextArea
621
+ label={t('鉴权json')}
622
+ name="key"
623
+ required
624
+ placeholder={'{\n' +
625
+ ' "type": "service_account",\n' +
626
+ ' "project_id": "abc-bcd-123-456",\n' +
627
+ ' "private_key_id": "123xxxxx456",\n' +
628
+ ' "private_key": "-----BEGIN PRIVATE KEY-----xxxx\n' +
629
+ ' "client_email": "xxx@developer.gserviceaccount.com",\n' +
630
+ ' "client_id": "111222333",\n' +
631
+ ' "auth_uri": "https://accounts.google.com/o/oauth2/auth",\n' +
632
+ ' "token_uri": "https://oauth2.googleapis.com/token",\n' +
633
+ ' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' +
634
+ ' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' +
635
+ ' "universe_domain": "googleapis.com"\n' +
636
+ '}'}
637
+ onChange={(value) => {
638
+ handleInputChange('key', value);
639
+ }}
640
+ autosize={{ minRows: 10 }}
641
+ value={inputs.key}
642
+ autoComplete="new-password"
643
+ />
644
+ ) : (
645
+ <Input
646
+ label={t('密钥')}
647
+ name="key"
648
+ required
649
+ placeholder={t(type2secretPrompt(inputs.type))}
650
+ onChange={(value) => {
651
+ handleInputChange('key', value);
652
+ }}
653
+ value={inputs.key}
654
+ autoComplete="new-password"
655
+ />
656
+ )}
657
+ </>
658
+ )}
659
+ {!isEdit && (
660
+ <div style={{ marginTop: 10, display: 'flex' }}>
661
+ <Space>
662
+ <Checkbox
663
+ checked={batch}
664
+ label={t('批量创建')}
665
+ name="batch"
666
+ onChange={() => setBatch(!batch)}
667
+ />
668
+ <Typography.Text strong>{t('批量创建')}</Typography.Text>
669
+ </Space>
670
+ </div>
671
+ )}
672
+ {inputs.type === 22 && (
673
+ <>
674
+ <div style={{ marginTop: 10 }}>
675
+ <Typography.Text strong>{t('私有部署地址')}:</Typography.Text>
676
+ </div>
677
+ <Input
678
+ name="base_url"
679
+ placeholder={t('请输入私有部署地址,格式为:https://fastgpt.run/api/openapi')}
680
+ onChange={(value) => {
681
+ handleInputChange('base_url', value);
682
+ }}
683
+ value={inputs.base_url}
684
+ autoComplete="new-password"
685
+ />
686
+ </>
687
+ )}
688
+ {inputs.type === 36 && (
689
+ <>
690
+ <div style={{ marginTop: 10 }}>
691
+ <Typography.Text strong>
692
+ {t('注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用')}
693
+ </Typography.Text>
694
+ </div>
695
+ <Input
696
+ name="base_url"
697
+ placeholder={t('请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com')}
698
+ onChange={(value) => {
699
+ handleInputChange('base_url', value);
700
+ }}
701
+ value={inputs.base_url}
702
+ autoComplete="new-password"
703
+ />
704
+ </>
705
+ )}
706
+ <div style={{ marginTop: 10 }}>
707
+ <Typography.Text strong>{t('分组')}:</Typography.Text>
708
+ </div>
709
+ <Select
710
+ placeholder={t('请选择可以使用该渠道的分组')}
711
+ name="groups"
712
+ required
713
+ multiple
714
+ selection
715
+ allowAdditions
716
+ additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
717
+ onChange={(value) => {
718
+ handleInputChange('groups', value);
719
+ }}
720
+ value={inputs.groups}
721
+ autoComplete="new-password"
722
+ optionList={groupOptions}
723
+ />
724
+ {inputs.type === 18 && (
725
+ <>
726
+ <div style={{ marginTop: 10 }}>
727
+ <Typography.Text strong>模型版本:</Typography.Text>
728
+ </div>
729
+ <Input
730
+ name="other"
731
+ placeholder={
732
+ '请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
733
+ }
734
+ onChange={(value) => {
735
+ handleInputChange('other', value);
736
+ }}
737
+ value={inputs.other}
738
+ autoComplete="new-password"
739
+ />
740
+ </>
741
+ )}
742
+ {inputs.type === 41 && (
743
+ <>
744
+ <div style={{ marginTop: 10 }}>
745
+ <Typography.Text strong>{t('部署地区')}:</Typography.Text>
746
+ </div>
747
+ <TextArea
748
+ name="other"
749
+ placeholder={t('请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
750
+ '{\n' +
751
+ ' "default": "us-central1",\n' +
752
+ ' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
753
+ '}')}
754
+ autosize={{ minRows: 2 }}
755
+ onChange={(value) => {
756
+ handleInputChange('other', value);
757
+ }}
758
+ value={inputs.other}
759
+ autoComplete="new-password"
760
+ />
761
+ <Typography.Text
762
+ style={{
763
+ color: 'rgba(var(--semi-blue-5), 1)',
764
+ userSelect: 'none',
765
+ cursor: 'pointer'
766
+ }}
767
+ onClick={() => {
768
+ handleInputChange(
769
+ 'other',
770
+ JSON.stringify(REGION_EXAMPLE, null, 2)
771
+ );
772
+ }}
773
+ >
774
+ {t('填入模板')}
775
+ </Typography.Text>
776
+ </>
777
+ )}
778
+ {inputs.type === 21 && (
779
+ <>
780
+ <div style={{ marginTop: 10 }}>
781
+ <Typography.Text strong>知识库 ID:</Typography.Text>
782
+ </div>
783
+ <Input
784
+ label="知识库 ID"
785
+ name="other"
786
+ placeholder={'请输入知识库 ID,例如:123456'}
787
+ onChange={(value) => {
788
+ handleInputChange('other', value);
789
+ }}
790
+ value={inputs.other}
791
+ autoComplete="new-password"
792
+ />
793
+ </>
794
+ )}
795
+ {inputs.type === 39 && (
796
+ <>
797
+ <div style={{ marginTop: 10 }}>
798
+ <Typography.Text strong>Account ID:</Typography.Text>
799
+ </div>
800
+ <Input
801
+ name="other"
802
+ placeholder={
803
+ '请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'
804
+ }
805
+ onChange={(value) => {
806
+ handleInputChange('other', value);
807
+ }}
808
+ value={inputs.other}
809
+ autoComplete="new-password"
810
+ />
811
+ </>
812
+ )}
813
+ <div style={{ marginTop: 10 }}>
814
+ <Typography.Text strong>{t('模型')}:</Typography.Text>
815
+ </div>
816
+ <Select
817
+ placeholder={'请选择该渠道所支持的模型'}
818
+ name="models"
819
+ required
820
+ multiple
821
+ selection
822
+ filter
823
+ searchPosition='dropdown'
824
+ onChange={(value) => {
825
+ handleInputChange('models', value);
826
+ }}
827
+ value={inputs.models}
828
+ autoComplete="new-password"
829
+ optionList={modelOptions}
830
+ />
831
+ <div style={{ lineHeight: '40px', marginBottom: '12px' }}>
832
+ <Space>
833
+ <Button
834
+ type="primary"
835
+ onClick={() => {
836
+ handleInputChange('models', basicModels);
837
+ }}
838
+ >
839
+ {t('填入相关模型')}
840
+ </Button>
841
+ <Button
842
+ type="secondary"
843
+ onClick={() => {
844
+ handleInputChange('models', fullModels);
845
+ }}
846
+ >
847
+ {t('填入所有模型')}
848
+ </Button>
849
+ <Tooltip content={t('新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出')}>
850
+ <Button
851
+ type="tertiary"
852
+ onClick={() => {
853
+ fetchUpstreamModelList('models');
854
+ }}
855
+ >
856
+ {t('获取模型列表')}
857
+ </Button>
858
+ </Tooltip>
859
+ <Button
860
+ type="warning"
861
+ onClick={() => {
862
+ handleInputChange('models', []);
863
+ }}
864
+ >
865
+ {t('清除所有模型')}
866
+ </Button>
867
+ <Button
868
+ theme="light"
869
+ type="primary"
870
+ onClick={handleAddPrefix}
871
+ >
872
+ {t('一键添加前缀')}
873
+ </Button>
874
+ </Space>
875
+ <Input
876
+ addonAfter={
877
+ <Button type="primary" onClick={addCustomModels}>
878
+ {t('填入')}
879
+ </Button>
880
+ }
881
+ placeholder={t('自定义型名称')}
882
+ value={customModel}
883
+ onChange={(value) => {
884
+ setCustomModel(value.trim());
885
+ }}
886
+ />
887
+ </div>
888
+ <div style={{ marginTop: 10 }}>
889
+ <Typography.Text strong>{t('模型重定向')}:</Typography.Text>
890
+ </div>
891
+ <TextArea
892
+ placeholder={t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') + `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
893
+ name="model_mapping"
894
+ onChange={(value) => {
895
+ handleInputChange('model_mapping', value);
896
+ }}
897
+ autosize
898
+ value={inputs.model_mapping}
899
+ autoComplete="new-password"
900
+ />
901
+ <Typography.Text
902
+ style={{
903
+ color: 'rgba(var(--semi-blue-5), 1)',
904
+ userSelect: 'none',
905
+ cursor: 'pointer'
906
+ }}
907
+ onClick={() => {
908
+ handleInputChange(
909
+ 'model_mapping',
910
+ JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
911
+ );
912
+ }}
913
+ >
914
+ {t('填入模板')}
915
+ </Typography.Text>
916
+ <div style={{ marginTop: 10 }}>
917
+ <Typography.Text strong>
918
+ {t('渠道标签')}
919
+ </Typography.Text>
920
+ </div>
921
+ <Input
922
+ label={t('渠道标签')}
923
+ name="tag"
924
+ placeholder={t('渠道标签')}
925
+ onChange={(value) => {
926
+ handleInputChange('tag', value);
927
+ }}
928
+ value={inputs.tag}
929
+ autoComplete="new-password"
930
+ />
931
+ <div style={{ marginTop: 10 }}>
932
+ <Typography.Text strong>
933
+ {t('渠道优先级')}
934
+ </Typography.Text>
935
+ </div>
936
+ <Input
937
+ label={t('渠道优先级')}
938
+ name="priority"
939
+ placeholder={t('渠道优先级')}
940
+ onChange={(value) => {
941
+ const number = parseInt(value);
942
+ if (isNaN(number)) {
943
+ handleInputChange('priority', value);
944
+ } else {
945
+ handleInputChange('priority', number);
946
+ }
947
+ }}
948
+ value={inputs.priority}
949
+ autoComplete="new-password"
950
+ />
951
+ <div style={{ marginTop: 10 }}>
952
+ <Typography.Text strong>
953
+ {t('渠道权重')}
954
+ </Typography.Text>
955
+ </div>
956
+ <Input
957
+ label={t('渠道权重')}
958
+ name="weight"
959
+ placeholder={t('渠道权重')}
960
+ onChange={(value) => {
961
+ const number = parseInt(value);
962
+ if (isNaN(number)) {
963
+ handleInputChange('weight', value);
964
+ } else {
965
+ handleInputChange('weight', number);
966
+ }
967
+ }}
968
+ value={inputs.weight}
969
+ autoComplete="new-password"
970
+ />
971
+ <>
972
+ <div style={{ marginTop: 10 }}>
973
+ <Typography.Text strong>
974
+ {t('渠道额外设置')}:
975
+ </Typography.Text>
976
+ </div>
977
+ <TextArea
978
+ placeholder={t('此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:') + '\n{\n "force_format": true\n}'}
979
+ name="setting"
980
+ onChange={(value) => {
981
+ handleInputChange('setting', value);
982
+ }}
983
+ autosize
984
+ value={inputs.setting}
985
+ autoComplete="new-password"
986
+ />
987
+ <Space>
988
+ <Typography.Text
989
+ style={{
990
+ color: 'rgba(var(--semi-blue-5), 1)',
991
+ userSelect: 'none',
992
+ cursor: 'pointer'
993
+ }}
994
+ onClick={() => {
995
+ handleInputChange(
996
+ 'setting',
997
+ JSON.stringify({
998
+ force_format: true
999
+ }, null, 2)
1000
+ );
1001
+ }}
1002
+ >
1003
+ {t('填入模板')}
1004
+ </Typography.Text>
1005
+ <Typography.Text
1006
+ style={{
1007
+ color: 'rgba(var(--semi-blue-5), 1)',
1008
+ userSelect: 'none',
1009
+ cursor: 'pointer'
1010
+ }}
1011
+ onClick={() => {
1012
+ window.open('https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md');
1013
+ }}
1014
+ >
1015
+ {t('设置说明')}
1016
+ </Typography.Text>
1017
+ </Space>
1018
+ </>
1019
+ <>
1020
+ <div style={{ marginTop: 10 }}>
1021
+ <Typography.Text strong>
1022
+ {t('参数覆盖')}:
1023
+ </Typography.Text>
1024
+ </div>
1025
+ <TextArea
1026
+ placeholder={t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:') + '\n{\n "temperature": 0\n}'}
1027
+ name="setting"
1028
+ onChange={(value) => {
1029
+ handleInputChange('param_override', value);
1030
+ }}
1031
+ autosize
1032
+ value={inputs.param_override}
1033
+ autoComplete="new-password"
1034
+ />
1035
+ </>
1036
+ {inputs.type === 1 && (
1037
+ <>
1038
+ <div style={{ marginTop: 10 }}>
1039
+ <Typography.Text strong>{t('组织')}:</Typography.Text>
1040
+ </div>
1041
+ <Input
1042
+ label={t('组织,可选,不填则为默认组织')}
1043
+ name="openai_organization"
1044
+ placeholder={t('请输入组织org-xxx')}
1045
+ onChange={(value) => {
1046
+ handleInputChange('openai_organization', value);
1047
+ }}
1048
+ value={inputs.openai_organization}
1049
+ />
1050
+ </>
1051
+ )}
1052
+ <div style={{ marginTop: 10 }}>
1053
+ <Typography.Text strong>{t('默认测试模型')}:</Typography.Text>
1054
+ </div>
1055
+ <Input
1056
+ name="test_model"
1057
+ placeholder={t('不填则为模型列表第一个')}
1058
+ onChange={(value) => {
1059
+ handleInputChange('test_model', value);
1060
+ }}
1061
+ value={inputs.test_model}
1062
+ />
1063
+ <div style={{ marginTop: 10, display: 'flex' }}>
1064
+ <Space>
1065
+ <Checkbox
1066
+ name="auto_ban"
1067
+ checked={autoBan}
1068
+ onChange={() => {
1069
+ setAutoBan(!autoBan);
1070
+ }}
1071
+ />
1072
+ <Typography.Text strong>
1073
+ {t('是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:')}
1074
+ </Typography.Text>
1075
+ </Space>
1076
+ </div>
1077
+ <div style={{ marginTop: 10 }}>
1078
+ <Typography.Text strong>
1079
+ {t('状态码复写(仅影响本地判断,不修改返回到上游的状态码)')}
1080
+ </Typography.Text>
1081
+ </div>
1082
+ <TextArea
1083
+ placeholder={t('此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:') +
1084
+ '\n' + JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}
1085
+ name="status_code_mapping"
1086
+ onChange={(value) => {
1087
+ handleInputChange('status_code_mapping', value);
1088
+ }}
1089
+ autosize
1090
+ value={inputs.status_code_mapping}
1091
+ autoComplete="new-password"
1092
+ />
1093
+ <Typography.Text
1094
+ style={{
1095
+ color: 'rgba(var(--semi-blue-5), 1)',
1096
+ userSelect: 'none',
1097
+ cursor: 'pointer'
1098
+ }}
1099
+ onClick={() => {
1100
+ handleInputChange(
1101
+ 'status_code_mapping',
1102
+ JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
1103
+ );
1104
+ }}
1105
+ >
1106
+ {t('填入模板')}
1107
+ </Typography.Text>
1108
+ </Spin>
1109
+ </SideSheet>
1110
+ </>
1111
+ );
1112
+ };
1113
+
1114
+ export default EditChannel;