import React, { useEffect, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { API, isMobile, showError, showInfo, showSuccess, verifyJSON, } from '../../helpers'; import { CHANNEL_OPTIONS } from '../../constants'; import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import { SideSheet, Space, Spin, Button, Tooltip, Input, Typography, Select, TextArea, Checkbox, Banner, } from '@douyinfe/semi-ui'; import { Divider } from 'semantic-ui-react'; import { getChannelModels, loadChannelModels } from '../../components/utils.js'; import axios from 'axios'; const MODEL_MAPPING_EXAMPLE = { 'gpt-3.5-turbo-0301': 'gpt-3.5-turbo', 'gpt-4-0314': 'gpt-4', 'gpt-4-32k-0314': 'gpt-4-32k', }; const STATUS_CODE_MAPPING_EXAMPLE = { 400: '500', }; const REGION_EXAMPLE = { "default": "us-central1", "claude-3-5-sonnet-20240620": "europe-west1" } const fetchButtonTips = "1. 新建渠道时,请求通过当前浏览器发出;2. 编辑已有渠道,请求通过后端服务器发出" 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 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'], }; 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 handleInputChange = (name, value) => { 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("仅支持 OpenAI 接口格式") return; } setLoading(true) const models = inputs["models"] || [] let err = false; if (isEdit) { 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("请填写密钥") err = true } else { try { const host = new URL((inputs["base_url"] || "https://api.openai.com")) const url = `https://${host.hostname}/v1/models`; const key = inputs["key"]; const res = await axios.get(url, { headers: { 'Authorization': `Bearer ${key}` } }) if (res.data && res.data?.success) { models.push(...res.data.data.map((model) => model.id)) } else { err = true } } catch (error) { err = true } } } if (!err) { handleInputChange(name, Array.from(new Set(models))); showSuccess("获取模型列表成功"); } else { showError('获取模型列表失败'); } 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-3') || 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.key === 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('请填写渠道名称和渠道密钥!'); return; } if (inputs.models.length === 0) { showInfo('请至少选择一个模型!'); return; } if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) { showInfo('模型映射必须是合法的 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 === 3 && localInputs.other === '') { localInputs.other = '2023-06-01-preview'; } if (localInputs.type === 18 && localInputs.other === '') { localInputs.other = 'v2.1'; } let res; if (!Array.isArray(localInputs.models)) { showError('提交失败,请勿重复提交!'); 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('渠道更新成功!'); } else { showSuccess('渠道创建成功!'); 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('某些模型已存在!'); hasError = true; } }); if (hasError) return; // 如果有错误则终止操作 // 更新状态值 setModelOptions(localModelOptions); setCustomModel(''); handleInputChange('models', localModels); }; return ( <> {isEdit ? '更新渠道信息' : '创建新的渠道'} } headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} visible={props.visible} footer={
} closeIcon={null} onCancel={() => handleCancel()} width={isMobile() ? '100%' : 600} >
类型:
{ handleInputChange('base_url', value); }} value={inputs.base_url} autoComplete='new-password' />
默认 API 版本:
{ handleInputChange('other', value); }} value={inputs.other} autoComplete='new-password' /> )} {inputs.type === 8 && ( <>
如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。 } >
完整的 Base URL,支持变量{'{model}'}:
{ handleInputChange('base_url', value); }} value={inputs.base_url} autoComplete='new-password' /> )} {inputs.type === 36 && ( <>
注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用
{ handleInputChange('base_url', value); }} value={inputs.base_url} autoComplete='new-password' /> )}
名称:
{ handleInputChange('name', value); }} value={inputs.name} autoComplete='new-password' />
分组:
{ handleInputChange('other', value); }} value={inputs.other} autoComplete='new-password' /> )} {inputs.type === 41 && ( <>
部署地区: