Merge remote-tracking branch 'origin/multi_keys_channel' into alpha
# Conflicts: # web/src/components/table/LogsTable.js # web/src/i18n/locales/en.json # web/src/pages/Channel/EditChannel.js
This commit is contained in:
@@ -42,18 +42,20 @@ import {
|
||||
IconTreeTriangleDown,
|
||||
IconSearch,
|
||||
IconMore,
|
||||
IconList, IconDescend2
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { loadChannelModels, isMobile, copy } from '../../helpers';
|
||||
import EditTagModal from '../../pages/Channel/EditTagModal.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTableCompactMode } from '../../hooks/useTableCompactMode';
|
||||
import { FaRandom } from 'react-icons/fa';
|
||||
|
||||
const ChannelsTable = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let type2label = undefined;
|
||||
|
||||
const renderType = (type) => {
|
||||
const renderType = (type, channelInfo = undefined) => {
|
||||
if (!type2label) {
|
||||
type2label = new Map();
|
||||
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
|
||||
@@ -61,11 +63,30 @@ const ChannelsTable = () => {
|
||||
}
|
||||
type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' };
|
||||
}
|
||||
|
||||
let icon = getChannelIcon(type);
|
||||
|
||||
if (channelInfo?.is_multi_key) {
|
||||
icon = (
|
||||
channelInfo?.multi_key_mode === 'random' ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<FaRandom className="text-blue-500" />
|
||||
{icon}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<IconDescend2 className="text-blue-500" />
|
||||
{icon}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color={type2label[type]?.color}
|
||||
shape='circle'
|
||||
prefixIcon={getChannelIcon(type)}
|
||||
prefixIcon={icon}
|
||||
>
|
||||
{type2label[type]?.label}
|
||||
</Tag>
|
||||
@@ -84,7 +105,19 @@ const ChannelsTable = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderStatus = (status) => {
|
||||
const renderStatus = (status, channelInfo = undefined) => {
|
||||
if (channelInfo) {
|
||||
if (channelInfo.is_multi_key) {
|
||||
let keySize = channelInfo.multi_key_size;
|
||||
let enabledKeySize = keySize;
|
||||
if (channelInfo.multi_key_status_list) {
|
||||
// multi_key_status_list is a map, key is key, value is status
|
||||
// get multi_key_status_list length
|
||||
enabledKeySize = keySize - Object.keys(channelInfo.multi_key_status_list).length;
|
||||
}
|
||||
return renderMultiKeyStatus(status, keySize, enabledKeySize);
|
||||
}
|
||||
}
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
@@ -113,6 +146,36 @@ const ChannelsTable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderMultiKeyStatus = (status, keySize, enabledKeySize) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag color='green' shape='circle'>
|
||||
{t('已启用')} {enabledKeySize}/{keySize}
|
||||
</Tag>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Tag color='red' shape='circle'>
|
||||
{t('已禁用')} {enabledKeySize}/{keySize}
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Tag color='yellow' shape='circle'>
|
||||
{t('自动禁用')} {enabledKeySize}/{keySize}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='grey' shape='circle'>
|
||||
{t('未知状态')} {enabledKeySize}/{keySize}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const renderResponseTime = (responseTime) => {
|
||||
let time = responseTime / 1000;
|
||||
time = time.toFixed(2) + t(' 秒');
|
||||
@@ -279,6 +342,11 @@ const ChannelsTable = () => {
|
||||
dataIndex: 'type',
|
||||
render: (text, record, index) => {
|
||||
if (record.children === undefined) {
|
||||
if (record.channel_info) {
|
||||
if (record.channel_info.is_multi_key) {
|
||||
return <>{renderType(text, record.channel_info)}</>;
|
||||
}
|
||||
}
|
||||
return <>{renderType(text)}</>;
|
||||
} else {
|
||||
return <>{renderTagType()}</>;
|
||||
@@ -302,12 +370,12 @@ const ChannelsTable = () => {
|
||||
<Tooltip
|
||||
content={t('原因:') + reason + t(',时间:') + timestamp2string(time)}
|
||||
>
|
||||
{renderStatus(text)}
|
||||
{renderStatus(text, record.channel_info)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return renderStatus(text);
|
||||
return renderStatus(text, record.channel_info);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -524,24 +592,70 @@ const ChannelsTable = () => {
|
||||
/>
|
||||
</SplitButtonGroup>
|
||||
|
||||
{record.status === 1 ? (
|
||||
<Button
|
||||
theme='light'
|
||||
type='warning'
|
||||
size="small"
|
||||
onClick={() => manageChannel(record.id, 'disable', record)}
|
||||
{record.channel_info?.is_multi_key ? (
|
||||
<SplitButtonGroup
|
||||
aria-label={t('多密钥渠道操作项目组')}
|
||||
>
|
||||
{t('禁用')}
|
||||
</Button>
|
||||
{
|
||||
record.status === 1 ? (
|
||||
<Button
|
||||
theme='light'
|
||||
type='warning'
|
||||
size="small"
|
||||
onClick={() => manageChannel(record.id, 'disable', record)}
|
||||
>
|
||||
{t('禁用')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
theme='light'
|
||||
type='secondary'
|
||||
size="small"
|
||||
onClick={() => manageChannel(record.id, 'enable', record)}
|
||||
>
|
||||
{t('启用')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Dropdown
|
||||
trigger='click'
|
||||
position='bottomRight'
|
||||
menu={[
|
||||
{
|
||||
node: 'item',
|
||||
name: t('启用全部密钥'),
|
||||
onClick: () => manageChannel(record.id, 'enable_all', record),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
theme='light'
|
||||
type='secondary'
|
||||
size="small"
|
||||
icon={<IconTreeTriangleDown />}
|
||||
/>
|
||||
</Dropdown>
|
||||
</SplitButtonGroup>
|
||||
) : (
|
||||
<Button
|
||||
theme='light'
|
||||
type='secondary'
|
||||
size="small"
|
||||
onClick={() => manageChannel(record.id, 'enable', record)}
|
||||
>
|
||||
{t('启用')}
|
||||
</Button>
|
||||
record.status === 1 ? (
|
||||
<Button
|
||||
theme='light'
|
||||
type='warning'
|
||||
size="small"
|
||||
onClick={() => manageChannel(record.id, 'disable', record)}
|
||||
>
|
||||
{t('禁用')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
theme='light'
|
||||
type='secondary'
|
||||
size="small"
|
||||
onClick={() => manageChannel(record.id, 'enable', record)}
|
||||
>
|
||||
{t('启用')}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
|
||||
<Button
|
||||
@@ -951,6 +1065,11 @@ const ChannelsTable = () => {
|
||||
}
|
||||
res = await API.put('/api/channel/', data);
|
||||
break;
|
||||
case 'enable_all':
|
||||
data.channel_info = record.channel_info;
|
||||
data.channel_info.multi_key_status_list = {};
|
||||
res = await API.put('/api/channel/', data);
|
||||
break;
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
@@ -1882,7 +2001,6 @@ const ChannelsTable = () => {
|
||||
placeholder={t('请输入标签名称')}
|
||||
value={batchSetTagValue}
|
||||
onChange={(v) => setBatchSetTagValue(v)}
|
||||
size='large'
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<Typography.Text type='secondary'>
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
renderQuota,
|
||||
stringToColor,
|
||||
getLogOther,
|
||||
renderModelTag,
|
||||
renderModelTag
|
||||
} from '../../helpers';
|
||||
|
||||
import {
|
||||
@@ -356,21 +356,43 @@ const LogsTable = () => {
|
||||
dataIndex: 'channel',
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
let isMultiKey = false
|
||||
let multiKeyIndex = -1;
|
||||
let other = getLogOther(record.other);
|
||||
if (other?.admin_info) {
|
||||
let adminInfo = other.admin_info;
|
||||
if (adminInfo?.is_multi_key) {
|
||||
isMultiKey = true;
|
||||
multiKeyIndex = adminInfo.multi_key_index;
|
||||
}
|
||||
}
|
||||
|
||||
return isAdminUser ? (
|
||||
record.type === 0 || record.type === 2 || record.type === 5 ? (
|
||||
<div>
|
||||
<>
|
||||
{
|
||||
<Tooltip content={record.channel_name || '[未知]'}>
|
||||
<Tag
|
||||
color={colors[parseInt(text) % colors.length]}
|
||||
shape='circle'
|
||||
>
|
||||
{' '}
|
||||
{text}{' '}
|
||||
</Tag>
|
||||
<Space>
|
||||
<Tag
|
||||
color={colors[parseInt(text) % colors.length]}
|
||||
shape='circle'
|
||||
>
|
||||
{text}
|
||||
</Tag>
|
||||
{
|
||||
isMultiKey && (
|
||||
<Tag
|
||||
color={'white'}
|
||||
shape='circle'
|
||||
>
|
||||
{multiKeyIndex}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
</Space>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
|
||||
@@ -87,6 +87,7 @@ export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled
|
||||
|
||||
const payload = {
|
||||
model: inputs.model,
|
||||
group: inputs.group,
|
||||
messages: processedMessages,
|
||||
group: inputs.group,
|
||||
stream: inputs.stream,
|
||||
|
||||
@@ -1763,5 +1763,14 @@
|
||||
"生成数量必须大于0": "Generation quantity must be greater than 0",
|
||||
"可用端点类型": "Supported endpoint types",
|
||||
"未登录,使用默认分组倍率:": "Not logged in, using default group ratio: ",
|
||||
"该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置": "This server address will affect the payment callback address and the address displayed on the default homepage, please ensure correct configuration"
|
||||
"该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置": "This server address will affect the payment callback address and the address displayed on the default homepage, please ensure correct configuration",
|
||||
"密钥聚合模式": "Key aggregation mode",
|
||||
"随机": "Random",
|
||||
"轮询": "Polling",
|
||||
"密钥文件 (.json)": "Key file (.json)",
|
||||
"点击上传文件或拖拽文件到这里": "Click to upload file or drag and drop file here",
|
||||
"仅支持 JSON 文件,支持多文件": "Only JSON files are supported, multiple files are supported",
|
||||
"请上传密钥文件": "Please upload the key file",
|
||||
"请填写部署地区": "Please fill in the deployment region",
|
||||
"请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Please enter the deployment region, for example: us-central1\nSupports using model mapping format\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}"
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
Form,
|
||||
Row,
|
||||
Col,
|
||||
Upload,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { getChannelModels, copy, getChannelIcon } from '../../helpers';
|
||||
import {
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
IconSetting,
|
||||
IconCode,
|
||||
IconGlobe,
|
||||
IconBolt,
|
||||
} from '@douyinfe/semi-icons';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
@@ -100,10 +102,12 @@ const EditChannel = (props) => {
|
||||
priority: 0,
|
||||
weight: 0,
|
||||
tag: '',
|
||||
multi_key_mode: 'random',
|
||||
};
|
||||
const [batch, setBatch] = useState(false);
|
||||
const [multiToSingle, setMultiToSingle] = useState(false);
|
||||
const [multiKeyMode, setMultiKeyMode] = useState('random');
|
||||
const [autoBan, setAutoBan] = useState(true);
|
||||
// const [autoBan, setAutoBan] = useState(true);
|
||||
const [inputs, setInputs] = useState(originInputs);
|
||||
const [originModelOptions, setOriginModelOptions] = useState([]);
|
||||
const [modelOptions, setModelOptions] = useState([]);
|
||||
@@ -114,6 +118,10 @@ const EditChannel = (props) => {
|
||||
const [modalImageUrl, setModalImageUrl] = useState('');
|
||||
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
||||
const formApiRef = useRef(null);
|
||||
const [vertexKeys, setVertexKeys] = useState([]);
|
||||
const [vertexFileList, setVertexFileList] = useState([]);
|
||||
const vertexErroredNames = useRef(new Set()); // 避免重复报错
|
||||
const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false);
|
||||
const getInitValues = () => ({ ...originInputs });
|
||||
const handleInputChange = (name, value) => {
|
||||
if (formApiRef.current) {
|
||||
@@ -211,6 +219,19 @@ const EditChannel = (props) => {
|
||||
2,
|
||||
);
|
||||
}
|
||||
const chInfo = data.channel_info || {};
|
||||
const isMulti = chInfo.is_multi_key === true;
|
||||
setIsMultiKeyChannel(isMulti);
|
||||
if (isMulti) {
|
||||
setBatch(true);
|
||||
setMultiToSingle(true);
|
||||
const modeVal = chInfo.multi_key_mode || 'random';
|
||||
setMultiKeyMode(modeVal);
|
||||
data.multi_key_mode = modeVal;
|
||||
} else {
|
||||
setBatch(false);
|
||||
setMultiToSingle(false);
|
||||
}
|
||||
setInputs(data);
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValues(data);
|
||||
@@ -381,10 +402,76 @@ const EditChannel = (props) => {
|
||||
}
|
||||
}, [props.visible, channelId]);
|
||||
|
||||
const handleVertexUploadChange = ({ fileList }) => {
|
||||
(async () => {
|
||||
const validFiles = [];
|
||||
const keys = [];
|
||||
const errorNames = [];
|
||||
for (const item of fileList) {
|
||||
const fileObj = item.fileInstance;
|
||||
if (!fileObj) continue;
|
||||
try {
|
||||
const txt = await fileObj.text();
|
||||
keys.push(JSON.parse(txt));
|
||||
validFiles.push(item); // 仅合法文件加入列表
|
||||
} catch (err) {
|
||||
if (!vertexErroredNames.current.has(item.name)) {
|
||||
errorNames.push(item.name);
|
||||
vertexErroredNames.current.add(item.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setVertexKeys(keys);
|
||||
setVertexFileList(validFiles);
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('vertex_files', validFiles);
|
||||
}
|
||||
setInputs((prev) => ({ ...prev, vertex_files: validFiles }));
|
||||
|
||||
if (errorNames.length > 0) {
|
||||
showError(t('以下文件解析失败,已忽略:{{list}}', { list: errorNames.join(', ') }));
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
const formValues = formApiRef.current ? formApiRef.current.getValues() : {};
|
||||
let localInputs = { ...formValues };
|
||||
|
||||
if (localInputs.type === 41) {
|
||||
let keys = vertexKeys;
|
||||
if (keys.length === 0) {
|
||||
// 确保提交时也能解析,避免因异步延迟导致 keys 为空
|
||||
try {
|
||||
const parsed = await Promise.all(
|
||||
vertexFileList.map(async (item) => {
|
||||
const fileObj = item.fileInstance;
|
||||
if (!fileObj) return null;
|
||||
const txt = await fileObj.text();
|
||||
return JSON.parse(txt);
|
||||
})
|
||||
);
|
||||
keys = parsed.filter(Boolean);
|
||||
} catch (err) {
|
||||
showError(t('解析密钥文件失败: {{msg}}', { msg: err.message }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (keys.length === 0) {
|
||||
showInfo(t('请上传密钥文件!'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (batch) {
|
||||
localInputs.key = JSON.stringify(keys);
|
||||
} else {
|
||||
localInputs.key = JSON.stringify(keys[0]);
|
||||
}
|
||||
}
|
||||
delete localInputs.vertex_files;
|
||||
|
||||
if (!isEdit && (!localInputs.name || !localInputs.key)) {
|
||||
showInfo(t('请填写渠道名称和渠道密钥!'));
|
||||
return;
|
||||
@@ -410,13 +497,23 @@ const EditChannel = (props) => {
|
||||
localInputs.auto_ban = localInputs.auto_ban ? 1 : 0;
|
||||
localInputs.models = localInputs.models.join(',');
|
||||
localInputs.group = (localInputs.groups || []).join(',');
|
||||
|
||||
let mode = 'single';
|
||||
if (batch) {
|
||||
mode = multiToSingle ? 'multi_to_single' : 'batch';
|
||||
}
|
||||
|
||||
if (isEdit) {
|
||||
res = await API.put(`/api/channel/`, {
|
||||
...localInputs,
|
||||
id: parseInt(channelId),
|
||||
});
|
||||
} else {
|
||||
res = await API.post(`/api/channel/`, localInputs);
|
||||
res = await API.post(`/api/channel/`, {
|
||||
mode: mode,
|
||||
multi_key_mode: mode === 'multi_to_single' ? multiKeyMode : undefined,
|
||||
channel: localInputs,
|
||||
});
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
@@ -469,9 +566,31 @@ const EditChannel = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const batchAllowed = !isEdit && inputs.type !== 41;
|
||||
const batchAllowed = !isEdit || isMultiKeyChannel;
|
||||
const batchExtra = batchAllowed ? (
|
||||
<Checkbox checked={batch} onChange={() => setBatch(!batch)}>{t('批量创建')}</Checkbox>
|
||||
<Space>
|
||||
<Checkbox disabled={isEdit} checked={batch} onChange={() => {
|
||||
setBatch(!batch);
|
||||
if (batch) {
|
||||
setMultiToSingle(false);
|
||||
setMultiKeyMode('random');
|
||||
}
|
||||
}}>{t('批量创建')}</Checkbox>
|
||||
{batch && (
|
||||
<Checkbox disabled={isEdit} checked={multiToSingle} onChange={() => {
|
||||
setMultiToSingle(prev => !prev);
|
||||
setInputs(prev => {
|
||||
const newInputs = { ...prev };
|
||||
if (!multiToSingle) {
|
||||
newInputs.multi_key_mode = multiKeyMode;
|
||||
} else {
|
||||
delete newInputs.multi_key_mode;
|
||||
}
|
||||
return newInputs;
|
||||
});
|
||||
}}>{t('密钥聚合模式')}</Checkbox>
|
||||
)}
|
||||
</Space>
|
||||
) : null;
|
||||
|
||||
const channelOptionList = useMemo(
|
||||
@@ -571,52 +690,93 @@ const EditChannel = (props) => {
|
||||
/>
|
||||
|
||||
{batch ? (
|
||||
<Form.TextArea
|
||||
field='key'
|
||||
label={t('密钥')}
|
||||
placeholder={t('请输入密钥,一行一个')}
|
||||
rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
||||
autosize={{ minRows: 6, maxRows: 6 }}
|
||||
autoComplete='new-password'
|
||||
onChange={(value) => handleInputChange('key', value)}
|
||||
extraText={batchExtra}
|
||||
/>
|
||||
inputs.type === 41 ? (
|
||||
<Form.Upload
|
||||
field='vertex_files'
|
||||
label={t('密钥文件 (.json)')}
|
||||
accept='.json'
|
||||
multiple
|
||||
draggable
|
||||
dragIcon={<IconBolt />}
|
||||
dragMainText={t('点击上传文件或拖拽文件到这里')}
|
||||
dragSubText={t('仅支持 JSON 文件,支持多文件')}
|
||||
style={{ marginTop: 10 }}
|
||||
uploadTrigger='custom'
|
||||
beforeUpload={() => false}
|
||||
onChange={handleVertexUploadChange}
|
||||
fileList={vertexFileList}
|
||||
rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]}
|
||||
extraText={batchExtra}
|
||||
/>
|
||||
) : (
|
||||
<Form.TextArea
|
||||
field='key'
|
||||
label={t('密钥')}
|
||||
placeholder={t('请输入密钥,一行一个')}
|
||||
rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
||||
autosize
|
||||
autoComplete='new-password'
|
||||
onChange={(value) => handleInputChange('key', value)}
|
||||
extraText={batchExtra}
|
||||
showClear
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{inputs.type === 41 ? (
|
||||
<Form.TextArea
|
||||
field='key'
|
||||
label={t('密钥')}
|
||||
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' +
|
||||
'}'
|
||||
}
|
||||
rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
||||
autosize={{ minRows: 10 }}
|
||||
autoComplete='new-password'
|
||||
onChange={(value) => handleInputChange('key', value)}
|
||||
<Form.Upload
|
||||
field='vertex_files'
|
||||
label={t('密钥文件 (.json)')}
|
||||
accept='.json'
|
||||
draggable
|
||||
dragIcon={<IconBolt />}
|
||||
dragMainText={t('点击上传文件或拖拽文件到这里')}
|
||||
dragSubText={t('仅支持 JSON 文件')}
|
||||
style={{ marginTop: 10 }}
|
||||
uploadTrigger='custom'
|
||||
beforeUpload={() => false}
|
||||
onChange={handleVertexUploadChange}
|
||||
fileList={vertexFileList}
|
||||
rules={isEdit ? [] : [{ required: true, message: t('请上传密钥文件') }]}
|
||||
extraText={batchExtra}
|
||||
/>
|
||||
) : (
|
||||
<Form.Input
|
||||
field='key'
|
||||
label={t('密钥')}
|
||||
label={isEdit ? t('密钥(编辑模式下,保存的密钥不会显示)') : t('密钥')}
|
||||
placeholder={t(type2secretPrompt(inputs.type))}
|
||||
rules={isEdit ? [] : [{ required: true, message: t('请输入密钥') }]}
|
||||
autoComplete='new-password'
|
||||
onChange={(value) => handleInputChange('key', value)}
|
||||
extraText={batchExtra}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{batch && multiToSingle && (
|
||||
<>
|
||||
<Form.Select
|
||||
field='multi_key_mode'
|
||||
label={t('密钥聚合模式')}
|
||||
placeholder={t('请选择多密钥使用策略')}
|
||||
optionList={[
|
||||
{ label: t('随机'), value: 'random' },
|
||||
{ label: t('轮询'), value: 'polling' },
|
||||
]}
|
||||
style={{ width: '100%' }}
|
||||
value={inputs.multi_key_mode || 'random'}
|
||||
onChange={(value) => {
|
||||
setMultiKeyMode(value);
|
||||
handleInputChange('multi_key_mode', value);
|
||||
}}
|
||||
/>
|
||||
{inputs.multi_key_mode === 'polling' && (
|
||||
<Banner
|
||||
type='warning'
|
||||
description={t('轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能')}
|
||||
className='!rounded-lg mt-2'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -639,8 +799,9 @@ const EditChannel = (props) => {
|
||||
placeholder={t(
|
||||
'请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n "default": "us-central1",\n "claude-3-5-sonnet-20240620": "europe-west1"\n}'
|
||||
)}
|
||||
autosize={{ minRows: 2 }}
|
||||
autosize
|
||||
onChange={(value) => handleInputChange('other', value)}
|
||||
rules={[{ required: true, message: t('请填写部署地区') }]}
|
||||
extraText={
|
||||
<Text
|
||||
className="!text-semi-color-primary cursor-pointer"
|
||||
@@ -649,6 +810,7 @@ const EditChannel = (props) => {
|
||||
{t('填入模板')}
|
||||
</Text>
|
||||
}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user