Spaces:
Paused
Paused
| import React, { useEffect, useState } from 'react'; | |
| import { useNavigate } from 'react-router-dom'; | |
| import { useTranslation } from 'react-i18next'; | |
| import { | |
| API, | |
| isMobile, | |
| showError, | |
| showInfo, | |
| showSuccess, | |
| verifyJSON, | |
| } from '../../helpers'; | |
| import { CHANNEL_OPTIONS } from '../../constants'; | |
| import { | |
| SideSheet, | |
| Space, | |
| Spin, | |
| Button, | |
| Input, | |
| Typography, | |
| Select, | |
| TextArea, | |
| Checkbox, | |
| Banner, | |
| Modal, | |
| ImagePreview, | |
| Card, | |
| Tag, | |
| } from '@douyinfe/semi-ui'; | |
| import { getChannelModels } from '../../helpers'; | |
| import { | |
| IconSave, | |
| IconClose, | |
| IconServer, | |
| IconSetting, | |
| IconCode, | |
| IconGlobe, | |
| } from '@douyinfe/semi-icons'; | |
| const { Text, Title } = Typography; | |
| const MODEL_MAPPING_EXAMPLE = { | |
| 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125', | |
| }; | |
| const STATUS_CODE_MAPPING_EXAMPLE = { | |
| 400: '500', | |
| }; | |
| const REGION_EXAMPLE = { | |
| default: 'us-central1', | |
| 'claude-3-5-sonnet-20240620': 'europe-west1', | |
| }; | |
| function type2secretPrompt(type) { | |
| // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥') | |
| switch (type) { | |
| case 15: | |
| return '按照如下格式输入:APIKey|SecretKey'; | |
| case 18: | |
| return '按照如下格式输入:APPID|APISecret|APIKey'; | |
| case 22: | |
| return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041'; | |
| case 23: | |
| return '按照如下格式输入:AppId|SecretId|SecretKey'; | |
| case 33: | |
| return '按照如下格式输入:Ak|Sk|Region'; | |
| default: | |
| return '请输入渠道对应的鉴权密钥'; | |
| } | |
| } | |
| const EditChannel = (props) => { | |
| const { t } = useTranslation(); | |
| const navigate = useNavigate(); | |
| const channelId = props.editingChannel.id; | |
| const isEdit = channelId !== undefined; | |
| const [loading, setLoading] = useState(isEdit); | |
| const handleCancel = () => { | |
| props.handleClose(); | |
| }; | |
| const originInputs = { | |
| name: '', | |
| type: 1, | |
| key: '', | |
| openai_organization: '', | |
| max_input_tokens: 0, | |
| base_url: '', | |
| other: '', | |
| model_mapping: '', | |
| status_code_mapping: '', | |
| models: [], | |
| auto_ban: 1, | |
| test_model: '', | |
| groups: ['default'], | |
| priority: 0, | |
| weight: 0, | |
| tag: '', | |
| }; | |
| const [batch, setBatch] = useState(false); | |
| const [autoBan, setAutoBan] = useState(true); | |
| // const [autoBan, setAutoBan] = useState(true); | |
| const [inputs, setInputs] = useState(originInputs); | |
| const [originModelOptions, setOriginModelOptions] = useState([]); | |
| const [modelOptions, setModelOptions] = useState([]); | |
| const [groupOptions, setGroupOptions] = useState([]); | |
| const [basicModels, setBasicModels] = useState([]); | |
| const [fullModels, setFullModels] = useState([]); | |
| const [customModel, setCustomModel] = useState(''); | |
| const [modalImageUrl, setModalImageUrl] = useState(''); | |
| const [isModalOpenurl, setIsModalOpenurl] = useState(false); | |
| const handleInputChange = (name, value) => { | |
| if (name === 'base_url' && value.endsWith('/v1')) { | |
| Modal.confirm({ | |
| title: '警告', | |
| content: | |
| '不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?', | |
| onOk: () => { | |
| setInputs((inputs) => ({ ...inputs, [name]: value })); | |
| }, | |
| }); | |
| return; | |
| } | |
| setInputs((inputs) => ({ ...inputs, [name]: value })); | |
| if (name === 'type') { | |
| let localModels = []; | |
| switch (value) { | |
| case 2: | |
| localModels = [ | |
| 'mj_imagine', | |
| 'mj_variation', | |
| 'mj_reroll', | |
| 'mj_blend', | |
| 'mj_upscale', | |
| 'mj_describe', | |
| 'mj_uploads', | |
| ]; | |
| break; | |
| case 5: | |
| localModels = [ | |
| 'swap_face', | |
| 'mj_imagine', | |
| 'mj_variation', | |
| 'mj_reroll', | |
| 'mj_blend', | |
| 'mj_upscale', | |
| 'mj_describe', | |
| 'mj_zoom', | |
| 'mj_shorten', | |
| 'mj_modal', | |
| 'mj_inpaint', | |
| 'mj_custom_zoom', | |
| 'mj_high_variation', | |
| 'mj_low_variation', | |
| 'mj_pan', | |
| 'mj_uploads', | |
| ]; | |
| break; | |
| case 36: | |
| localModels = ['suno_music', 'suno_lyrics']; | |
| break; | |
| default: | |
| localModels = getChannelModels(value); | |
| break; | |
| } | |
| if (inputs.models.length === 0) { | |
| setInputs((inputs) => ({ ...inputs, models: localModels })); | |
| } | |
| setBasicModels(localModels); | |
| } | |
| //setAutoBan | |
| }; | |
| const loadChannel = async () => { | |
| setLoading(true); | |
| let res = await API.get(`/api/channel/${channelId}`); | |
| if (res === undefined) { | |
| return; | |
| } | |
| const { success, message, data } = res.data; | |
| if (success) { | |
| if (data.models === '') { | |
| data.models = []; | |
| } else { | |
| data.models = data.models.split(','); | |
| } | |
| if (data.group === '') { | |
| data.groups = []; | |
| } else { | |
| data.groups = data.group.split(','); | |
| } | |
| if (data.model_mapping !== '') { | |
| data.model_mapping = JSON.stringify( | |
| JSON.parse(data.model_mapping), | |
| null, | |
| 2, | |
| ); | |
| } | |
| setInputs(data); | |
| if (data.auto_ban === 0) { | |
| setAutoBan(false); | |
| } else { | |
| setAutoBan(true); | |
| } | |
| setBasicModels(getChannelModels(data.type)); | |
| // console.log(data); | |
| } else { | |
| showError(message); | |
| } | |
| setLoading(false); | |
| }; | |
| const fetchUpstreamModelList = async (name) => { | |
| // if (inputs['type'] !== 1) { | |
| // showError(t('仅支持 OpenAI 接口格式')); | |
| // return; | |
| // } | |
| setLoading(true); | |
| const models = inputs['models'] || []; | |
| let err = false; | |
| if (isEdit) { | |
| // 如果是编辑模式,使用已有的channel id获取模型列表 | |
| const res = await API.get('/api/channel/fetch_models/' + channelId); | |
| if (res.data && res.data?.success) { | |
| models.push(...res.data.data); | |
| } else { | |
| err = true; | |
| } | |
| } else { | |
| // 如果是新建模式,通过后端代理获取模型列表 | |
| if (!inputs?.['key']) { | |
| showError(t('请填写密钥')); | |
| err = true; | |
| } else { | |
| try { | |
| const res = await API.post('/api/channel/fetch_models', { | |
| base_url: inputs['base_url'], | |
| type: inputs['type'], | |
| key: inputs['key'], | |
| }); | |
| if (res.data && res.data.success) { | |
| models.push(...res.data.data); | |
| } else { | |
| err = true; | |
| } | |
| } catch (error) { | |
| console.error('Error fetching models:', error); | |
| err = true; | |
| } | |
| } | |
| } | |
| if (!err) { | |
| handleInputChange(name, Array.from(new Set(models))); | |
| showSuccess(t('获取模型列表成功')); | |
| } else { | |
| showError(t('获取模型列表失败')); | |
| } | |
| setLoading(false); | |
| }; | |
| const fetchModels = async () => { | |
| try { | |
| let res = await API.get(`/api/channel/models`); | |
| let localModelOptions = res.data.data.map((model) => ({ | |
| label: model.id, | |
| value: model.id, | |
| })); | |
| setOriginModelOptions(localModelOptions); | |
| setFullModels(res.data.data.map((model) => model.id)); | |
| setBasicModels( | |
| res.data.data | |
| .filter((model) => { | |
| return model.id.startsWith('gpt-') || model.id.startsWith('text-'); | |
| }) | |
| .map((model) => model.id), | |
| ); | |
| } catch (error) { | |
| showError(error.message); | |
| } | |
| }; | |
| const fetchGroups = async () => { | |
| try { | |
| let res = await API.get(`/api/group/`); | |
| if (res === undefined) { | |
| return; | |
| } | |
| setGroupOptions( | |
| res.data.data.map((group) => ({ | |
| label: group, | |
| value: group, | |
| })), | |
| ); | |
| } catch (error) { | |
| showError(error.message); | |
| } | |
| }; | |
| useEffect(() => { | |
| let localModelOptions = [...originModelOptions]; | |
| inputs.models.forEach((model) => { | |
| if (!localModelOptions.find((option) => option.label === model)) { | |
| localModelOptions.push({ | |
| label: model, | |
| value: model, | |
| }); | |
| } | |
| }); | |
| setModelOptions(localModelOptions); | |
| }, [originModelOptions, inputs.models]); | |
| useEffect(() => { | |
| fetchModels().then(); | |
| fetchGroups().then(); | |
| if (isEdit) { | |
| loadChannel().then(() => { }); | |
| } else { | |
| setInputs(originInputs); | |
| let localModels = getChannelModels(inputs.type); | |
| setBasicModels(localModels); | |
| setInputs((inputs) => ({ ...inputs, models: localModels })); | |
| } | |
| }, [props.editingChannel.id]); | |
| const submit = async () => { | |
| if (!isEdit && (inputs.name === '' || inputs.key === '')) { | |
| showInfo(t('请填写渠道名称和渠道密钥!')); | |
| return; | |
| } | |
| if (inputs.models.length === 0) { | |
| showInfo(t('请至少选择一个模型!')); | |
| return; | |
| } | |
| if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) { | |
| showInfo(t('模型映射必须是合法的 JSON 格式!')); | |
| return; | |
| } | |
| let localInputs = { ...inputs }; | |
| if (localInputs.base_url && localInputs.base_url.endsWith('/')) { | |
| localInputs.base_url = localInputs.base_url.slice( | |
| 0, | |
| localInputs.base_url.length - 1, | |
| ); | |
| } | |
| if (localInputs.type === 18 && localInputs.other === '') { | |
| localInputs.other = 'v2.1'; | |
| } | |
| let res; | |
| if (!Array.isArray(localInputs.models)) { | |
| showError(t('提交失败,请勿重复提交!')); | |
| handleCancel(); | |
| return; | |
| } | |
| localInputs.auto_ban = autoBan ? 1 : 0; | |
| localInputs.models = localInputs.models.join(','); | |
| localInputs.group = localInputs.groups.join(','); | |
| if (isEdit) { | |
| res = await API.put(`/api/channel/`, { | |
| ...localInputs, | |
| id: parseInt(channelId), | |
| }); | |
| } else { | |
| res = await API.post(`/api/channel/`, localInputs); | |
| } | |
| const { success, message } = res.data; | |
| if (success) { | |
| if (isEdit) { | |
| showSuccess(t('渠道更新成功!')); | |
| } else { | |
| showSuccess(t('渠道创建成功!')); | |
| setInputs(originInputs); | |
| } | |
| props.refresh(); | |
| props.handleClose(); | |
| } else { | |
| showError(message); | |
| } | |
| }; | |
| const addCustomModels = () => { | |
| if (customModel.trim() === '') return; | |
| const modelArray = customModel.split(',').map((model) => model.trim()); | |
| let localModels = [...inputs.models]; | |
| let localModelOptions = [...modelOptions]; | |
| let hasError = false; | |
| modelArray.forEach((model) => { | |
| if (model && !localModels.includes(model)) { | |
| localModels.push(model); | |
| localModelOptions.push({ | |
| key: model, | |
| text: model, | |
| value: model, | |
| }); | |
| } else if (model) { | |
| showError(t('某些模型已存在!')); | |
| hasError = true; | |
| } | |
| }); | |
| if (hasError) return; | |
| setModelOptions(localModelOptions); | |
| setCustomModel(''); | |
| handleInputChange('models', localModels); | |
| }; | |
| const handleAddPrefix = () => { | |
| const channelName = inputs.name; | |
| const currentModels = inputs.models; // This is an array of strings | |
| if (!channelName || !currentModels || currentModels.length === 0) { | |
| showError(t('请先填写渠道名称并选择模型!')); | |
| return; | |
| } | |
| const prefixedModels = currentModels.map( | |
| (model) => `${channelName}/${model}`, | |
| ); | |
| const mapping = {}; | |
| prefixedModels.forEach((prefixedModel, index) => { | |
| mapping[prefixedModel] = currentModels[index]; | |
| }); | |
| const modelMappingJson = JSON.stringify(mapping, null, 2); // Pretty print JSON | |
| handleInputChange('models', prefixedModels); | |
| handleInputChange('model_mapping', modelMappingJson); | |
| showSuccess(t('模型前缀添加成功!')); | |
| }; | |
| return ( | |
| <> | |
| <SideSheet | |
| placement={isEdit ? 'right' : 'left'} | |
| title={ | |
| <Space> | |
| <Tag color="blue" shape="circle">{isEdit ? t('编辑') : t('新建')}</Tag> | |
| <Title heading={4} className="m-0"> | |
| {isEdit ? t('更新渠道信息') : t('创建新的渠道')} | |
| </Title> | |
| </Space> | |
| } | |
| headerStyle={{ | |
| borderBottom: '1px solid var(--semi-color-border)', | |
| padding: '24px' | |
| }} | |
| bodyStyle={{ | |
| backgroundColor: 'var(--semi-color-bg-0)', | |
| padding: '0' | |
| }} | |
| visible={props.visible} | |
| width={isMobile() ? '100%' : 600} | |
| footer={ | |
| <div className="flex justify-end bg-white"> | |
| <Space> | |
| <Button | |
| theme="solid" | |
| size="large" | |
| className="!rounded-full" | |
| onClick={submit} | |
| icon={<IconSave />} | |
| > | |
| {t('提交')} | |
| </Button> | |
| <Button | |
| theme="light" | |
| size="large" | |
| className="!rounded-full" | |
| type="primary" | |
| onClick={handleCancel} | |
| icon={<IconClose />} | |
| > | |
| {t('取消')} | |
| </Button> | |
| </Space> | |
| </div> | |
| } | |
| closeIcon={null} | |
| onCancel={() => handleCancel()} | |
| > | |
| <Spin spinning={loading}> | |
| <div className="p-6"> | |
| <Card className="!rounded-2xl shadow-sm border-0 mb-6"> | |
| <div className="flex items-center mb-4 p-6 rounded-xl" style={{ | |
| background: 'linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #3b82f6 100%)', | |
| position: 'relative' | |
| }}> | |
| <div className="absolute inset-0 overflow-hidden"> | |
| <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div> | |
| <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div> | |
| </div> | |
| <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative"> | |
| <IconServer size="large" style={{ color: '#ffffff' }} /> | |
| </div> | |
| <div className="relative"> | |
| <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('基本信息')}</Text> | |
| <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('渠道的基本配置信息')}</div> | |
| </div> | |
| </div> | |
| <div className="space-y-4"> | |
| <div> | |
| <Text strong className="block mb-2">{t('类型')}</Text> | |
| <Select | |
| name='type' | |
| required | |
| optionList={CHANNEL_OPTIONS} | |
| value={inputs.type} | |
| onChange={(value) => handleInputChange('type', value)} | |
| style={{ width: '100%' }} | |
| filter | |
| searchPosition='dropdown' | |
| placeholder={t('请选择渠道类型')} | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| <div> | |
| <Text strong className="block mb-2">{t('名称')}</Text> | |
| <Input | |
| required | |
| name='name' | |
| placeholder={t('请为渠道命名')} | |
| onChange={(value) => { | |
| handleInputChange('name', value); | |
| }} | |
| value={inputs.name} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| <div> | |
| <Text strong className="block mb-2">{t('密钥')}</Text> | |
| {batch ? ( | |
| <TextArea | |
| name='key' | |
| required | |
| placeholder={t('请输入密钥,一行一个')} | |
| onChange={(value) => { | |
| handleInputChange('key', value); | |
| }} | |
| value={inputs.key} | |
| style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }} | |
| autoComplete='new-password' | |
| className="!rounded-lg" | |
| /> | |
| ) : ( | |
| <> | |
| {inputs.type === 41 ? ( | |
| <TextArea | |
| name='key' | |
| required | |
| placeholder={ | |
| '{\n' + | |
| ' "type": "service_account",\n' + | |
| ' "project_id": "abc-bcd-123-456",\n' + | |
| ' "private_key_id": "123xxxxx456",\n' + | |
| ' "private_key": "-----BEGIN PRIVATE KEY-----xxxx\n' + | |
| ' "client_email": "xxx@developer.gserviceaccount.com",\n' + | |
| ' "client_id": "111222333",\n' + | |
| ' "auth_uri": "https://accounts.google.com/o/oauth2/auth",\n' + | |
| ' "token_uri": "https://oauth2.googleapis.com/token",\n' + | |
| ' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' + | |
| ' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' + | |
| ' "universe_domain": "googleapis.com"\n' + | |
| '}' | |
| } | |
| onChange={(value) => { | |
| handleInputChange('key', value); | |
| }} | |
| autosize={{ minRows: 10 }} | |
| value={inputs.key} | |
| autoComplete='new-password' | |
| className="!rounded-lg font-mono" | |
| /> | |
| ) : ( | |
| <Input | |
| name='key' | |
| required | |
| placeholder={t(type2secretPrompt(inputs.type))} | |
| onChange={(value) => { | |
| handleInputChange('key', value); | |
| }} | |
| value={inputs.key} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| )} | |
| </> | |
| )} | |
| </div> | |
| {!isEdit && ( | |
| <div className="flex items-center"> | |
| <Checkbox | |
| checked={batch} | |
| onChange={() => setBatch(!batch)} | |
| /> | |
| <Text strong className="ml-2">{t('批量创建')}</Text> | |
| </div> | |
| )} | |
| </div> | |
| </Card> | |
| {/* API Configuration Card */} | |
| <Card className="!rounded-2xl shadow-sm border-0 mb-6"> | |
| <div className="flex items-center mb-4 p-6 rounded-xl" style={{ | |
| background: 'linear-gradient(135deg, #065f46 0%, #059669 50%, #10b981 100%)', | |
| position: 'relative' | |
| }}> | |
| <div className="absolute inset-0 overflow-hidden"> | |
| <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div> | |
| <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div> | |
| </div> | |
| <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative"> | |
| <IconGlobe size="large" style={{ color: '#ffffff' }} /> | |
| </div> | |
| <div className="relative"> | |
| <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('API 配置')}</Text> | |
| <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('API 地址和相关配置')}</div> | |
| </div> | |
| </div> | |
| <div className="space-y-4"> | |
| {inputs.type === 40 && ( | |
| <Banner | |
| type='info' | |
| description={ | |
| <div> | |
| <Text strong>{t('邀请链接')}:</Text> | |
| <Text | |
| link | |
| underline | |
| className="ml-2 cursor-pointer" | |
| onClick={() => window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')} | |
| > | |
| https://cloud.siliconflow.cn/i/hij0YNTZ | |
| </Text> | |
| </div> | |
| } | |
| className='!rounded-lg' | |
| /> | |
| )} | |
| {inputs.type === 3 && ( | |
| <> | |
| <Banner | |
| type='warning' | |
| description={t('2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的"."')} | |
| className='!rounded-lg' | |
| /> | |
| <div> | |
| <Text strong className="block mb-2">AZURE_OPENAI_ENDPOINT</Text> | |
| <Input | |
| name='azure_base_url' | |
| placeholder={t('请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com')} | |
| onChange={(value) => handleInputChange('base_url', value)} | |
| value={inputs.base_url} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| <div> | |
| <Text strong className="block mb-2">{t('默认 API 版本')}</Text> | |
| <Input | |
| name='azure_other' | |
| placeholder={t('请输入默认 API 版本,例如:2025-04-01-preview')} | |
| onChange={(value) => handleInputChange('other', value)} | |
| value={inputs.other} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| </> | |
| )} | |
| {inputs.type === 8 && ( | |
| <> | |
| <Banner | |
| type='warning' | |
| description={t('如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。')} | |
| className='!rounded-lg' | |
| /> | |
| <div> | |
| <Text strong className="block mb-2">{t('完整的 Base URL,支持变量{model}')}</Text> | |
| <Input | |
| name='base_url' | |
| placeholder={t('请输入完整的URL,例如:https://api.openai.com/v1/chat/completions')} | |
| onChange={(value) => handleInputChange('base_url', value)} | |
| value={inputs.base_url} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| </> | |
| )} | |
| {inputs.type === 37 && ( | |
| <Banner | |
| type='warning' | |
| description={t('Dify渠道只适配chatflow和agent,并且agent不支持图片!')} | |
| className='!rounded-lg' | |
| /> | |
| )} | |
| {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && ( | |
| <div> | |
| <Text strong className="block mb-2">{t('API地址')}</Text> | |
| <Input | |
| name='base_url' | |
| placeholder={t('此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/')} | |
| onChange={(value) => handleInputChange('base_url', value)} | |
| value={inputs.base_url} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| <Text type="tertiary" className="mt-1 text-xs"> | |
| {t('对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写')} | |
| </Text> | |
| </div> | |
| )} | |
| {inputs.type === 22 && ( | |
| <div> | |
| <Text strong className="block mb-2">{t('私有部署地址')}</Text> | |
| <Input | |
| name='base_url' | |
| placeholder={t('请输入私有部署地址,格式为:https://fastgpt.run/api/openapi')} | |
| onChange={(value) => handleInputChange('base_url', value)} | |
| value={inputs.base_url} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| )} | |
| {inputs.type === 36 && ( | |
| <div> | |
| <Text strong className="block mb-2"> | |
| {t('注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用')} | |
| </Text> | |
| <Input | |
| name='base_url' | |
| placeholder={t('请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com')} | |
| onChange={(value) => handleInputChange('base_url', value)} | |
| value={inputs.base_url} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| </Card> | |
| {/* Model Configuration Card */} | |
| <Card className="!rounded-2xl shadow-sm border-0 mb-6"> | |
| <div className="flex items-center mb-4 p-6 rounded-xl" style={{ | |
| background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)', | |
| position: 'relative' | |
| }}> | |
| <div className="absolute inset-0 overflow-hidden"> | |
| <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div> | |
| <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div> | |
| </div> | |
| <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative"> | |
| <IconCode size="large" style={{ color: '#ffffff' }} /> | |
| </div> | |
| <div className="relative"> | |
| <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('模型配置')}</Text> | |
| <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('模型选择和映射设置')}</div> | |
| </div> | |
| </div> | |
| <div className="space-y-4"> | |
| <div> | |
| <Text strong className="block mb-2">{t('模型')}</Text> | |
| <Select | |
| placeholder={t('请选择该渠道所支持的模型')} | |
| name='models' | |
| required | |
| multiple | |
| selection | |
| filter | |
| searchPosition='dropdown' | |
| onChange={(value) => handleInputChange('models', value)} | |
| value={inputs.models} | |
| autoComplete='new-password' | |
| optionList={modelOptions} | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| <div className="flex flex-wrap gap-2"> | |
| <Button | |
| type='primary' | |
| onClick={() => handleInputChange('models', basicModels)} | |
| size="large" | |
| className="!rounded-lg" | |
| > | |
| {t('填入相关模型')} | |
| </Button> | |
| <Button | |
| type='secondary' | |
| onClick={() => handleInputChange('models', fullModels)} | |
| size="large" | |
| className="!rounded-lg" | |
| > | |
| {t('填入所有模型')} | |
| </Button> | |
| <Button | |
| type='tertiary' | |
| onClick={() => fetchUpstreamModelList('models')} | |
| size="large" | |
| className="!rounded-lg" | |
| > | |
| {t('获取模型列表')} | |
| </Button> | |
| <Button | |
| theme='light' | |
| type='primary' | |
| onClick={handleAddPrefix} | |
| size='large' | |
| className='!rounded-lg' | |
| > | |
| {t('一键添加前缀')} | |
| </Button> | |
| <Button | |
| type='warning' | |
| onClick={() => handleInputChange('models', [])} | |
| size="large" | |
| className="!rounded-lg" | |
| > | |
| {t('清除所有模型')} | |
| </Button> | |
| </div> | |
| <div> | |
| <Input | |
| addonAfter={ | |
| <Button type='primary' onClick={addCustomModels} className="!rounded-r-lg"> | |
| {t('填入')} | |
| </Button> | |
| } | |
| placeholder={t('输入自定义模型名称')} | |
| value={customModel} | |
| onChange={(value) => setCustomModel(value.trim())} | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| <div> | |
| <Text strong className="block mb-2">{t('模型重定向')}</Text> | |
| <TextArea | |
| placeholder={ | |
| t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') + | |
| `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}` | |
| } | |
| name='model_mapping' | |
| onChange={(value) => handleInputChange('model_mapping', value)} | |
| autosize | |
| value={inputs.model_mapping} | |
| autoComplete='new-password' | |
| className="!rounded-lg font-mono" | |
| /> | |
| <Text | |
| className="!text-semi-color-primary cursor-pointer mt-1 block" | |
| onClick={() => handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))} | |
| > | |
| {t('填入模板')} | |
| </Text> | |
| </div> | |
| <div> | |
| <Text strong className="block mb-2">{t('默认测试模型')}</Text> | |
| <Input | |
| name='test_model' | |
| placeholder={t('不填则为模型列表第一个')} | |
| onChange={(value) => handleInputChange('test_model', value)} | |
| value={inputs.test_model} | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| </div> | |
| </Card> | |
| {/* Advanced Settings Card */} | |
| <Card className="!rounded-2xl shadow-sm border-0 mb-6"> | |
| <div className="flex items-center mb-4 p-6 rounded-xl" style={{ | |
| background: 'linear-gradient(135deg, #92400e 0%, #d97706 50%, #f59e0b 100%)', | |
| position: 'relative' | |
| }}> | |
| <div className="absolute inset-0 overflow-hidden"> | |
| <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div> | |
| <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div> | |
| </div> | |
| <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative"> | |
| <IconSetting size="large" style={{ color: '#ffffff' }} /> | |
| </div> | |
| <div className="relative"> | |
| <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('高级设置')}</Text> | |
| <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('渠道的高级配置选项')}</div> | |
| </div> | |
| </div> | |
| <div className="space-y-4"> | |
| <div> | |
| <Text strong className="block mb-2">{t('分组')}</Text> | |
| <Select | |
| placeholder={t('请选择可以使用该渠道的分组')} | |
| name='groups' | |
| required | |
| multiple | |
| selection | |
| allowAdditions | |
| additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')} | |
| onChange={(value) => handleInputChange('groups', value)} | |
| value={inputs.groups} | |
| autoComplete='new-password' | |
| optionList={groupOptions} | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| {inputs.type === 18 && ( | |
| <div> | |
| <Text strong className="block mb-2">{t('模型版本')}</Text> | |
| <Input | |
| name='other' | |
| placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'} | |
| onChange={(value) => handleInputChange('other', value)} | |
| value={inputs.other} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| )} | |
| {inputs.type === 41 && ( | |
| <div> | |
| <Text strong className="block mb-2">{t('部署地区')}</Text> | |
| <TextArea | |
| name='other' | |
| placeholder={t( | |
| '请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' + | |
| '{\n' + | |
| ' "default": "us-central1",\n' + | |
| ' "claude-3-5-sonnet-20240620": "europe-west1"\n' + | |
| '}' | |
| )} | |
| autosize={{ minRows: 2 }} | |
| onChange={(value) => handleInputChange('other', value)} | |
| value={inputs.other} | |
| autoComplete='new-password' | |
| className="!rounded-lg font-mono" | |
| /> | |
| <Text | |
| className="!text-semi-color-primary cursor-pointer mt-1 block" | |
| onClick={() => handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))} | |
| > | |
| {t('填入模板')} | |
| </Text> | |
| </div> | |
| )} | |
| {inputs.type === 21 && ( | |
| <div> | |
| <Text strong className="block mb-2">{t('知识库 ID')}</Text> | |
| <Input | |
| name='other' | |
| placeholder={'请输入知识库 ID,例如:123456'} | |
| onChange={(value) => handleInputChange('other', value)} | |
| value={inputs.other} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| )} | |
| {inputs.type === 39 && ( | |
| <div> | |
| <Text strong className="block mb-2">Account ID</Text> | |
| <Input | |
| name='other' | |
| placeholder={'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'} | |
| onChange={(value) => handleInputChange('other', value)} | |
| value={inputs.other} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| )} | |
| {inputs.type === 49 && ( | |
| <div> | |
| <Text strong className="block mb-2">{t('智能体ID')}</Text> | |
| <Input | |
| name='other' | |
| placeholder={'请输入智能体ID,例如:7342866812345'} | |
| onChange={(value) => handleInputChange('other', value)} | |
| value={inputs.other} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| )} | |
| <div> | |
| <Text strong className="block mb-2">{t('渠道标签')}</Text> | |
| <Input | |
| name='tag' | |
| placeholder={t('渠道标签')} | |
| onChange={(value) => handleInputChange('tag', value)} | |
| value={inputs.tag} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| <div> | |
| <Text strong className="block mb-2">{t('渠道优先级')}</Text> | |
| <Input | |
| name='priority' | |
| placeholder={t('渠道优先级')} | |
| onChange={(value) => { | |
| const number = parseInt(value); | |
| if (isNaN(number)) { | |
| handleInputChange('priority', value); | |
| } else { | |
| handleInputChange('priority', number); | |
| } | |
| }} | |
| value={inputs.priority} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| <div> | |
| <Text strong className="block mb-2">{t('渠道权重')}</Text> | |
| <Input | |
| name='weight' | |
| placeholder={t('渠道权重')} | |
| onChange={(value) => { | |
| const number = parseInt(value); | |
| if (isNaN(number)) { | |
| handleInputChange('weight', value); | |
| } else { | |
| handleInputChange('weight', number); | |
| } | |
| }} | |
| value={inputs.weight} | |
| autoComplete='new-password' | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| </div> | |
| <div> | |
| <Text strong className="block mb-2">{t('渠道额外设置')}</Text> | |
| <TextArea | |
| placeholder={ | |
| t('此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:') + | |
| '\n{\n "force_format": true\n}' | |
| } | |
| name='setting' | |
| onChange={(value) => handleInputChange('setting', value)} | |
| autosize | |
| value={inputs.setting} | |
| autoComplete='new-password' | |
| className="!rounded-lg font-mono" | |
| /> | |
| <div className="flex gap-2 mt-1"> | |
| <Text | |
| className="!text-semi-color-primary cursor-pointer" | |
| onClick={() => { | |
| handleInputChange( | |
| 'setting', | |
| JSON.stringify({ force_format: true }, null, 2), | |
| ); | |
| }} | |
| > | |
| {t('填入模板')} | |
| </Text> | |
| <Text | |
| className="!text-semi-color-primary cursor-pointer" | |
| onClick={() => { | |
| window.open( | |
| 'https://github.com/QuantumNous/new-api/blob/main/docs/channel/other_setting.md', | |
| ); | |
| }} | |
| > | |
| {t('设置说明')} | |
| </Text> | |
| </div> | |
| </div> | |
| <div> | |
| <Text strong className="block mb-2">{t('参数覆盖')}</Text> | |
| <TextArea | |
| placeholder={ | |
| t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:') + | |
| '\n{\n "temperature": 0\n}' | |
| } | |
| name='param_override' | |
| onChange={(value) => handleInputChange('param_override', value)} | |
| autosize | |
| value={inputs.param_override} | |
| autoComplete='new-password' | |
| className="!rounded-lg font-mono" | |
| /> | |
| </div> | |
| {inputs.type === 1 && ( | |
| <div> | |
| <Text strong className="block mb-2">{t('组织')}</Text> | |
| <Input | |
| name='openai_organization' | |
| placeholder={t('请输入组织org-xxx')} | |
| onChange={(value) => handleInputChange('openai_organization', value)} | |
| value={inputs.openai_organization} | |
| size="large" | |
| className="!rounded-lg" | |
| /> | |
| <Text type="tertiary" className="mt-1 text-xs"> | |
| {t('组织,可选,不填则为默认组织')} | |
| </Text> | |
| </div> | |
| )} | |
| <div className="flex items-center"> | |
| <Checkbox | |
| checked={autoBan} | |
| onChange={() => setAutoBan(!autoBan)} | |
| /> | |
| <Text strong className="ml-2"> | |
| {t('是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道')} | |
| </Text> | |
| </div> | |
| <div> | |
| <Text strong className="block mb-2"> | |
| {t('状态码复写(仅影响本地判断,不修改返回到上游的状态码)')} | |
| </Text> | |
| <TextArea | |
| placeholder={ | |
| t('此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:') + | |
| '\n' + | |
| JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2) | |
| } | |
| name='status_code_mapping' | |
| onChange={(value) => handleInputChange('status_code_mapping', value)} | |
| autosize | |
| value={inputs.status_code_mapping} | |
| autoComplete='new-password' | |
| className="!rounded-lg font-mono" | |
| /> | |
| <Text | |
| className="!text-semi-color-primary cursor-pointer mt-1 block" | |
| onClick={() => { | |
| handleInputChange( | |
| 'status_code_mapping', | |
| JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2), | |
| ); | |
| }} | |
| > | |
| {t('填入模板')} | |
| </Text> | |
| </div> | |
| </div> | |
| </Card> | |
| </div> | |
| </Spin> | |
| <ImagePreview | |
| src={modalImageUrl} | |
| visible={isModalOpenurl} | |
| onVisibleChange={(visible) => setIsModalOpenurl(visible)} | |
| /> | |
| </SideSheet> | |
| </> | |
| ); | |
| }; | |
| export default EditChannel; | |