|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useEffect, useState, useRef } from 'react'; |
|
|
import { Button, Col, Form, Row, Spin, Card, Typography } from '@douyinfe/semi-ui'; |
|
|
import { |
|
|
compareObjects, |
|
|
API, |
|
|
showError, |
|
|
showSuccess, |
|
|
showWarning, |
|
|
} from '../../../helpers'; |
|
|
import { useTranslation } from 'react-i18next'; |
|
|
import { Server, Cloud, Zap, ArrowUpRight } from 'lucide-react'; |
|
|
|
|
|
const { Text } = Typography; |
|
|
|
|
|
export default function SettingModelDeployment(props) { |
|
|
const { t } = useTranslation(); |
|
|
|
|
|
const [loading, setLoading] = useState(false); |
|
|
const [inputs, setInputs] = useState({ |
|
|
'model_deployment.ionet.api_key': '', |
|
|
'model_deployment.ionet.enabled': false, |
|
|
}); |
|
|
const refForm = useRef(); |
|
|
const [inputsRow, setInputsRow] = useState({ |
|
|
'model_deployment.ionet.api_key': '', |
|
|
'model_deployment.ionet.enabled': false, |
|
|
}); |
|
|
const [testing, setTesting] = useState(false); |
|
|
|
|
|
const testApiKey = async () => { |
|
|
const apiKey = inputs['model_deployment.ionet.api_key']; |
|
|
if (!apiKey || apiKey.trim() === '') { |
|
|
showError(t('请先填写 API Key')); |
|
|
return; |
|
|
} |
|
|
|
|
|
const getLocalizedMessage = (message) => { |
|
|
switch (message) { |
|
|
case 'invalid request payload': |
|
|
return t('请求参数无效'); |
|
|
case 'api_key is required': |
|
|
return t('请先填写 API Key'); |
|
|
case 'failed to validate api key': |
|
|
return t('API Key 验证失败'); |
|
|
default: |
|
|
return message; |
|
|
} |
|
|
}; |
|
|
|
|
|
setTesting(true); |
|
|
try { |
|
|
const response = await API.post( |
|
|
'/api/deployments/test-connection', |
|
|
{ |
|
|
api_key: apiKey.trim(), |
|
|
}, |
|
|
{ |
|
|
skipErrorHandler: true, |
|
|
}, |
|
|
); |
|
|
|
|
|
if (response?.data?.success) { |
|
|
showSuccess(t('API Key 验证成功!连接到 io.net 服务正常')); |
|
|
} else { |
|
|
const rawMessage = response?.data?.message; |
|
|
const localizedMessage = rawMessage |
|
|
? getLocalizedMessage(rawMessage) |
|
|
: t('API Key 验证失败'); |
|
|
showError(localizedMessage); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('io.net API test error:', error); |
|
|
|
|
|
if (error?.code === 'ERR_NETWORK') { |
|
|
showError(t('网络连接失败,请检查网络设置或稍后重试')); |
|
|
} else { |
|
|
const rawMessage = |
|
|
error?.response?.data?.message || |
|
|
error?.message || |
|
|
''; |
|
|
const localizedMessage = rawMessage |
|
|
? getLocalizedMessage(rawMessage) |
|
|
: t('未知错误'); |
|
|
showError(t('测试失败:') + localizedMessage); |
|
|
} |
|
|
} finally { |
|
|
setTesting(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
function onSubmit() { |
|
|
|
|
|
if (inputs['model_deployment.ionet.enabled'] && |
|
|
(!inputs['model_deployment.ionet.api_key'] || inputs['model_deployment.ionet.api_key'].trim() === '')) { |
|
|
return showError(t('启用 io.net 部署时必须填写 API Key')); |
|
|
} |
|
|
|
|
|
const updateArray = compareObjects(inputs, inputsRow); |
|
|
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么')); |
|
|
|
|
|
const requestQueue = updateArray.map((item) => { |
|
|
let value = String(inputs[item.key]); |
|
|
return API.put('/api/option/', { |
|
|
key: item.key, |
|
|
value, |
|
|
}); |
|
|
}); |
|
|
|
|
|
setLoading(true); |
|
|
Promise.all(requestQueue) |
|
|
.then((res) => { |
|
|
if (requestQueue.length === 1) { |
|
|
if (res.includes(undefined)) return; |
|
|
} else if (requestQueue.length > 1) { |
|
|
if (res.includes(undefined)) |
|
|
return showError(t('部分保存失败,请重试')); |
|
|
} |
|
|
showSuccess(t('保存成功')); |
|
|
|
|
|
setInputsRow(structuredClone(inputs)); |
|
|
props.refresh(); |
|
|
}) |
|
|
.catch(() => { |
|
|
showError(t('保存失败,请重试')); |
|
|
}) |
|
|
.finally(() => { |
|
|
setLoading(false); |
|
|
}); |
|
|
} |
|
|
|
|
|
useEffect(() => { |
|
|
if (props.options) { |
|
|
const defaultInputs = { |
|
|
'model_deployment.ionet.api_key': '', |
|
|
'model_deployment.ionet.enabled': false, |
|
|
}; |
|
|
|
|
|
const currentInputs = {}; |
|
|
for (let key in defaultInputs) { |
|
|
if (props.options.hasOwnProperty(key)) { |
|
|
currentInputs[key] = props.options[key]; |
|
|
} else { |
|
|
currentInputs[key] = defaultInputs[key]; |
|
|
} |
|
|
} |
|
|
|
|
|
setInputs(currentInputs); |
|
|
setInputsRow(structuredClone(currentInputs)); |
|
|
refForm.current?.setValues(currentInputs); |
|
|
} |
|
|
}, [props.options]); |
|
|
|
|
|
return ( |
|
|
<> |
|
|
<Spin spinning={loading}> |
|
|
<Form |
|
|
values={inputs} |
|
|
getFormApi={(formAPI) => (refForm.current = formAPI)} |
|
|
style={{ marginBottom: 15 }} |
|
|
> |
|
|
<Form.Section |
|
|
text={ |
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> |
|
|
<span>{t('模型部署设置')}</span> |
|
|
</div> |
|
|
} |
|
|
> |
|
|
{/*<Text */} |
|
|
{/* type="secondary" */} |
|
|
{/* size="small"*/} |
|
|
{/* style={{ */} |
|
|
{/* display: 'block', */} |
|
|
{/* marginBottom: '20px',*/} |
|
|
{/* color: 'var(--semi-color-text-2)'*/} |
|
|
{/* }}*/} |
|
|
{/*>*/} |
|
|
{/* {t('配置模型部署服务提供商的API密钥和启用状态')}*/} |
|
|
{/*</Text>*/} |
|
|
|
|
|
<Card |
|
|
title={ |
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> |
|
|
<Cloud size={18} /> |
|
|
<span>io.net</span> |
|
|
</div> |
|
|
} |
|
|
bodyStyle={{ padding: '20px' }} |
|
|
style={{ marginBottom: '16px' }} |
|
|
> |
|
|
<Row gutter={24}> |
|
|
<Col xs={24} lg={14}> |
|
|
<div |
|
|
style={{ |
|
|
display: 'flex', |
|
|
flexDirection: 'column', |
|
|
gap: '16px', |
|
|
}} |
|
|
> |
|
|
<Form.Switch |
|
|
label={t('启用 io.net 部署')} |
|
|
field={'model_deployment.ionet.enabled'} |
|
|
onChange={(value) => |
|
|
setInputs({ |
|
|
...inputs, |
|
|
'model_deployment.ionet.enabled': value, |
|
|
}) |
|
|
} |
|
|
extraText={t('启用后可接入 io.net GPU 资源')} |
|
|
/> |
|
|
<Form.Input |
|
|
label={t('API Key')} |
|
|
field={'model_deployment.ionet.api_key'} |
|
|
placeholder={t('请输入 io.net API Key')} |
|
|
onChange={(value) => |
|
|
setInputs({ |
|
|
...inputs, |
|
|
'model_deployment.ionet.api_key': value, |
|
|
}) |
|
|
} |
|
|
disabled={!inputs['model_deployment.ionet.enabled']} |
|
|
extraText={t('请使用 Project 为 io.cloud 的密钥')} |
|
|
mode="password" |
|
|
/> |
|
|
<div style={{ display: 'flex', gap: '12px' }}> |
|
|
<Button |
|
|
type="outline" |
|
|
size="small" |
|
|
icon={<Zap size={16} />} |
|
|
onClick={testApiKey} |
|
|
loading={testing} |
|
|
disabled={ |
|
|
!inputs['model_deployment.ionet.enabled'] || |
|
|
!inputs['model_deployment.ionet.api_key'] || |
|
|
inputs['model_deployment.ionet.api_key'].trim() === '' |
|
|
} |
|
|
style={{ |
|
|
height: '32px', |
|
|
fontSize: '13px', |
|
|
borderRadius: '6px', |
|
|
fontWeight: '500', |
|
|
borderColor: testing |
|
|
? 'var(--semi-color-primary)' |
|
|
: 'var(--semi-color-border)', |
|
|
color: testing |
|
|
? 'var(--semi-color-primary)' |
|
|
: 'var(--semi-color-text-0)', |
|
|
}} |
|
|
> |
|
|
{testing ? t('连接测试中...') : t('测试连接')} |
|
|
</Button> |
|
|
</div> |
|
|
</div> |
|
|
</Col> |
|
|
<Col xs={24} lg={10}> |
|
|
<div |
|
|
style={{ |
|
|
background: 'var(--semi-color-fill-0)', |
|
|
padding: '16px', |
|
|
borderRadius: '8px', |
|
|
border: '1px solid var(--semi-color-border)', |
|
|
height: '100%', |
|
|
display: 'flex', |
|
|
flexDirection: 'column', |
|
|
gap: '12px', |
|
|
justifyContent: 'space-between', |
|
|
}} |
|
|
> |
|
|
<div> |
|
|
<Text strong style={{ display: 'block', marginBottom: '8px' }}> |
|
|
{t('获取 io.net API Key')} |
|
|
</Text> |
|
|
<ul |
|
|
style={{ |
|
|
margin: 0, |
|
|
paddingLeft: '18px', |
|
|
display: 'flex', |
|
|
flexDirection: 'column', |
|
|
gap: '6px', |
|
|
color: 'var(--semi-color-text-2)', |
|
|
fontSize: '13px', |
|
|
lineHeight: 1.6, |
|
|
}} |
|
|
> |
|
|
<li>{t('访问 io.net 控制台的 API Keys 页面')}</li> |
|
|
<li>{t('创建或选择密钥时,将 Project 设置为 io.cloud')}</li> |
|
|
<li>{t('复制生成的密钥并粘贴到此处')}</li> |
|
|
</ul> |
|
|
</div> |
|
|
<Button |
|
|
icon={<ArrowUpRight size={16} />} |
|
|
type="primary" |
|
|
theme="solid" |
|
|
style={{ width: '100%' }} |
|
|
onClick={() => |
|
|
window.open('https://ai.io.net/ai/api-keys', '_blank') |
|
|
} |
|
|
> |
|
|
{t('前往 io.net API Keys')} |
|
|
</Button> |
|
|
</div> |
|
|
</Col> |
|
|
</Row> |
|
|
</Card> |
|
|
|
|
|
<Row> |
|
|
<Button size='default' type="primary" onClick={onSubmit}> |
|
|
{t('保存设置')} |
|
|
</Button> |
|
|
</Row> |
|
|
</Form.Section> |
|
|
</Form> |
|
|
</Spin> |
|
|
</> |
|
|
); |
|
|
} |
|
|
|