import React, { useEffect, useState, useRef } 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, Typography, Checkbox, Banner, Modal, ImagePreview, Card, Tag, Avatar, Form, Row, Col, } from '@douyinfe/semi-ui'; import { getChannelModels, copy } 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'; case 50: return '按照如下格式输入: AccessKey|SecretKey'; case 51: return '按照如下格式输入: Access Key ID|Secret Access Key'; 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 formApiRef = useRef(null); const getInitValues = () => ({ ...originInputs }); const handleInputChange = (name, value) => { if (formApiRef.current) { formApiRef.current.setValue(name, value); } if (name === 'models' && Array.isArray(value)) { value = Array.from(new Set(value.map((m) => (m || '').trim()))); } 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_video', 'mj_edits', '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 (formApiRef.current) { formApiRef.current.setValues(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`); const localModelOptions = res.data.data.map((model) => { const id = (model.id || '').trim(); return { key: id, label: id, value: 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(() => { const modelMap = new Map(); originModelOptions.forEach(option => { const v = (option.value || '').trim(); if (!modelMap.has(v)) { modelMap.set(v, option); } }); inputs.models.forEach(model => { const v = (model || '').trim(); if (!modelMap.has(v)) { modelMap.set(v, { key: v, label: v, value: v, }); } }); setModelOptions(Array.from(modelMap.values())); }, [originModelOptions, inputs.models]); useEffect(() => { fetchModels().then(); fetchGroups().then(); if (!isEdit) { setInputs(originInputs); if (formApiRef.current) { formApiRef.current.setValues(originInputs); } let localModels = getChannelModels(inputs.type); setBasicModels(localModels); setInputs((inputs) => ({ ...inputs, models: localModels })); } }, [props.editingChannel.id]); useEffect(() => { if (formApiRef.current) { formApiRef.current.setValues(inputs); } }, [inputs]); useEffect(() => { if (props.visible) { if (isEdit) { loadChannel(); } else { formApiRef.current?.setValues(getInitValues()); } } else { formApiRef.current?.reset(); } }, [props.visible, channelId]); const submit = async () => { const formValues = formApiRef.current ? formApiRef.current.getValues() : {}; let localInputs = { ...formValues }; if (!isEdit && (!localInputs.name || !localInputs.key)) { showInfo(t('请填写渠道名称和渠道密钥!')); return; } if (!Array.isArray(localInputs.models) || localInputs.models.length === 0) { showInfo(t('请至少选择一个模型!')); return; } if (localInputs.model_mapping && localInputs.model_mapping !== '' && !verifyJSON(localInputs.model_mapping)) { showInfo(t('模型映射必须是合法的 JSON 格式!')); return; } 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; localInputs.auto_ban = localInputs.auto_ban ? 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]; const addedModels = []; modelArray.forEach((model) => { if (model && !localModels.includes(model)) { localModels.push(model); localModelOptions.push({ key: model, label: model, value: model, }); addedModels.push(model); } }); setModelOptions(localModelOptions); setCustomModel(''); handleInputChange('models', localModels); if (addedModels.length > 0) { showSuccess( t('已新增 {{count}} 个模型:{{list}}', { count: addedModels.length, list: addedModels.join(', '), }) ); } else { showInfo(t('未发现新增模型')); } }; const batchAllowed = !isEdit && inputs.type !== 41; const batchExtra = batchAllowed ? ( setBatch(!batch)}>{t('批量创建')} ) : null; return ( <> {isEdit ? t('编辑') : t('新建')} {isEdit ? t('更新渠道信息') : t('创建新的渠道')} } bodyStyle={{ padding: '0' }} visible={props.visible} width={isMobile() ? '100%' : 600} footer={
} closeIcon={null} onCancel={() => handleCancel()} >
(formApiRef.current = api)} onSubmit={submit} > {() => (
{/* Header: Basic Info */}
{t('基本信息')}
{t('渠道的基本配置信息')}
handleInputChange('type', value)} /> handleInputChange('name', value)} autoComplete='new-password' /> {batch ? ( handleInputChange('key', value)} extraText={batchExtra} /> ) : ( <> {inputs.type === 41 ? ( handleInputChange('key', value)} extraText={batchExtra} /> ) : ( handleInputChange('key', value)} extraText={batchExtra} /> )} )}
{/* API Configuration Card */} {/* Header: API Config */}
{t('API 配置')}
{t('API 地址和相关配置')}
{inputs.type === 40 && ( {t('邀请链接')}: window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')} > https://cloud.siliconflow.cn/i/hij0YNTZ
} className='!rounded-lg' /> )} {inputs.type === 3 && ( <>
handleInputChange('base_url', value)} showClear />
handleInputChange('other', value)} showClear />
)} {inputs.type === 8 && ( <>
handleInputChange('base_url', value)} showClear />
)} {inputs.type === 37 && ( )} {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && (
handleInputChange('base_url', value)} showClear extraText={t('对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写')} />
)} {inputs.type === 22 && (
handleInputChange('base_url', value)} showClear />
)} {inputs.type === 36 && (
handleInputChange('base_url', value)} showClear />
)} {/* Model Configuration Card */} {/* Header: Model Config */}
{t('模型配置')}
{t('模型选择和映射设置')}
handleInputChange('models', value)} extraText={( {isEdit && ( )} )} /> setCustomModel(value.trim())} value={customModel} suffix={ } /> handleInputChange('test_model', value)} showClear /> handleInputChange('model_mapping', value)} extraText={ handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))} > {t('填入模板')} } showClear />
{/* Advanced Settings Card */} {/* Header: Advanced Settings */}
{t('高级设置')}
{t('渠道的高级配置选项')}
handleInputChange('groups', value)} /> {inputs.type === 18 && ( handleInputChange('other', value)} showClear /> )} {inputs.type === 41 && ( handleInputChange('other', value)} extraText={ handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))} > {t('填入模板')} } /> )} {inputs.type === 21 && ( handleInputChange('other', value)} showClear /> )} {inputs.type === 39 && ( handleInputChange('other', value)} showClear /> )} {inputs.type === 49 && ( handleInputChange('other', value)} showClear /> )} {inputs.type === 1 && ( handleInputChange('openai_organization', value)} /> )} handleInputChange('tag', value)} /> handleInputChange('priority', value)} style={{ width: '100%' }} /> handleInputChange('weight', value)} style={{ width: '100%' }} /> setAutoBan(val)} extraText={t('仅当自动禁用开启时有效,关闭后不会自动禁用该渠道')} initValue={autoBan} /> handleInputChange('param_override', value)} extraText={ handleInputChange('param_override', JSON.stringify({ temperature: 0 }, null, 2))} > {t('填入模板')} } showClear /> handleInputChange('status_code_mapping', value)} extraText={ handleInputChange('status_code_mapping', JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2))} > {t('填入模板')} } showClear /> handleInputChange('setting', value)} extraText={( handleInputChange('setting', JSON.stringify({ force_format: true }, null, 2))} > {t('填入模板')} window.open('https://github.com/QuantumNous/new-api/blob/main/docs/channel/other_setting.md')} > {t('设置说明')} )} showClear />
)}
setIsModalOpenurl(visible)} />
); }; export default EditChannel;