Merge pull request #927 from QuentinHsu/refactor-system-setting
# Conflicts: # web/src/App.js # web/src/components/ModelSetting.js # web/src/components/PersonalSetting.js # web/src/components/SystemSetting.js # web/src/pages/Channel/EditChannel.js
This commit is contained in:
@@ -6,8 +6,9 @@ import {
|
||||
isMobile,
|
||||
showError,
|
||||
showInfo,
|
||||
showSuccess, showWarning,
|
||||
verifyJSON
|
||||
showSuccess,
|
||||
showWarning,
|
||||
verifyJSON,
|
||||
} from '../../helpers';
|
||||
import { CHANNEL_OPTIONS } from '../../constants';
|
||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||
@@ -22,21 +23,22 @@ import {
|
||||
Select,
|
||||
TextArea,
|
||||
Checkbox,
|
||||
Banner, Modal
|
||||
Banner,
|
||||
Modal,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { getChannelModels, loadChannelModels } from '../../components/utils.js';
|
||||
|
||||
const MODEL_MAPPING_EXAMPLE = {
|
||||
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
|
||||
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
|
||||
};
|
||||
|
||||
const STATUS_CODE_MAPPING_EXAMPLE = {
|
||||
400: '500'
|
||||
400: '500',
|
||||
};
|
||||
|
||||
const REGION_EXAMPLE = {
|
||||
'default': 'us-central1',
|
||||
'claude-3-5-sonnet-20240620': 'europe-west1'
|
||||
default: 'us-central1',
|
||||
'claude-3-5-sonnet-20240620': 'europe-west1',
|
||||
};
|
||||
|
||||
function type2secretPrompt(type) {
|
||||
@@ -82,7 +84,7 @@ const EditChannel = (props) => {
|
||||
groups: ['default'],
|
||||
priority: 0,
|
||||
weight: 0,
|
||||
tag: ''
|
||||
tag: '',
|
||||
};
|
||||
const [batch, setBatch] = useState(false);
|
||||
const [autoBan, setAutoBan] = useState(true);
|
||||
@@ -98,12 +100,13 @@ const EditChannel = (props) => {
|
||||
if (name === 'base_url' && value.endsWith('/v1')) {
|
||||
Modal.confirm({
|
||||
title: '警告',
|
||||
content: '不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
|
||||
content:
|
||||
'不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
|
||||
onOk: () => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
}
|
||||
})
|
||||
return
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
if (name === 'type') {
|
||||
@@ -117,7 +120,7 @@ const EditChannel = (props) => {
|
||||
'mj_blend',
|
||||
'mj_upscale',
|
||||
'mj_describe',
|
||||
'mj_uploads'
|
||||
'mj_uploads',
|
||||
];
|
||||
break;
|
||||
case 5:
|
||||
@@ -137,14 +140,11 @@ const EditChannel = (props) => {
|
||||
'mj_high_variation',
|
||||
'mj_low_variation',
|
||||
'mj_pan',
|
||||
'mj_uploads'
|
||||
'mj_uploads',
|
||||
];
|
||||
break;
|
||||
case 36:
|
||||
localModels = [
|
||||
'suno_music',
|
||||
'suno_lyrics'
|
||||
];
|
||||
localModels = ['suno_music', 'suno_lyrics'];
|
||||
break;
|
||||
default:
|
||||
localModels = getChannelModels(value);
|
||||
@@ -180,7 +180,7 @@ const EditChannel = (props) => {
|
||||
data.model_mapping = JSON.stringify(
|
||||
JSON.parse(data.model_mapping),
|
||||
null,
|
||||
2
|
||||
2,
|
||||
);
|
||||
}
|
||||
setInputs(data);
|
||||
@@ -197,7 +197,6 @@ const EditChannel = (props) => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
const fetchUpstreamModelList = async (name) => {
|
||||
// if (inputs['type'] !== 1) {
|
||||
// showError(t('仅支持 OpenAI 接口格式'));
|
||||
@@ -225,9 +224,9 @@ const EditChannel = (props) => {
|
||||
const res = await API.post('/api/channel/fetch_models', {
|
||||
base_url: inputs['base_url'],
|
||||
type: inputs['type'],
|
||||
key: inputs['key']
|
||||
key: inputs['key'],
|
||||
});
|
||||
|
||||
|
||||
if (res.data && res.data.success) {
|
||||
models.push(...res.data.data);
|
||||
} else {
|
||||
@@ -254,7 +253,7 @@ const EditChannel = (props) => {
|
||||
let res = await API.get(`/api/channel/models`);
|
||||
let localModelOptions = res.data.data.map((model) => ({
|
||||
label: model.id,
|
||||
value: model.id
|
||||
value: model.id,
|
||||
}));
|
||||
setOriginModelOptions(localModelOptions);
|
||||
setFullModels(res.data.data.map((model) => model.id));
|
||||
@@ -263,7 +262,7 @@ const EditChannel = (props) => {
|
||||
.filter((model) => {
|
||||
return model.id.startsWith('gpt-') || model.id.startsWith('text-');
|
||||
})
|
||||
.map((model) => model.id)
|
||||
.map((model) => model.id),
|
||||
);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
@@ -279,8 +278,8 @@ const EditChannel = (props) => {
|
||||
setGroupOptions(
|
||||
res.data.data.map((group) => ({
|
||||
label: group,
|
||||
value: group
|
||||
}))
|
||||
value: group,
|
||||
})),
|
||||
);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
@@ -293,7 +292,7 @@ const EditChannel = (props) => {
|
||||
if (!localModelOptions.find((option) => option.label === model)) {
|
||||
localModelOptions.push({
|
||||
label: model,
|
||||
value: model
|
||||
value: model,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -304,7 +303,7 @@ const EditChannel = (props) => {
|
||||
fetchModels().then();
|
||||
fetchGroups().then();
|
||||
if (isEdit) {
|
||||
loadChannel().then(() => {});
|
||||
loadChannel().then(() => { });
|
||||
} else {
|
||||
setInputs(originInputs);
|
||||
let localModels = getChannelModels(inputs.type);
|
||||
@@ -330,7 +329,7 @@ const EditChannel = (props) => {
|
||||
if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
|
||||
localInputs.base_url = localInputs.base_url.slice(
|
||||
0,
|
||||
localInputs.base_url.length - 1
|
||||
localInputs.base_url.length - 1,
|
||||
);
|
||||
}
|
||||
if (localInputs.type === 18 && localInputs.other === '') {
|
||||
@@ -348,7 +347,7 @@ const EditChannel = (props) => {
|
||||
if (isEdit) {
|
||||
res = await API.put(`/api/channel/`, {
|
||||
...localInputs,
|
||||
id: parseInt(channelId)
|
||||
id: parseInt(channelId),
|
||||
});
|
||||
} else {
|
||||
res = await API.post(`/api/channel/`, localInputs);
|
||||
@@ -382,7 +381,7 @@ const EditChannel = (props) => {
|
||||
localModelOptions.push({
|
||||
key: model,
|
||||
text: model,
|
||||
value: model
|
||||
value: model,
|
||||
});
|
||||
} else if (model) {
|
||||
showError(t('某些模型已存在!'));
|
||||
@@ -397,14 +396,15 @@ const EditChannel = (props) => {
|
||||
handleInputChange('models', localModels);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<SideSheet
|
||||
maskClosable={false}
|
||||
placement={isEdit ? 'right' : 'left'}
|
||||
title={
|
||||
<Title level={3}>{isEdit ? t('更新渠道信息') : t('创建新的渠道')}</Title>
|
||||
<Title level={3}>
|
||||
{isEdit ? t('更新渠道信息') : t('创建新的渠道')}
|
||||
</Title>
|
||||
}
|
||||
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
||||
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
||||
@@ -412,11 +412,11 @@ const EditChannel = (props) => {
|
||||
footer={
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Space>
|
||||
<Button theme="solid" size={'large'} onClick={submit}>
|
||||
<Button theme='solid' size={'large'} onClick={submit}>
|
||||
{t('提交')}
|
||||
</Button>
|
||||
<Button
|
||||
theme="solid"
|
||||
theme='solid'
|
||||
size={'large'}
|
||||
type={'tertiary'}
|
||||
onClick={handleCancel}
|
||||
@@ -432,11 +432,10 @@ const EditChannel = (props) => {
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
|
||||
<Typography.Text strong>{t('类型')}:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
name="type"
|
||||
name='type'
|
||||
required
|
||||
optionList={CHANNEL_OPTIONS}
|
||||
value={inputs.type}
|
||||
@@ -449,17 +448,17 @@ const EditChannel = (props) => {
|
||||
{inputs.type === 40 && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Banner
|
||||
type="info"
|
||||
type='info'
|
||||
description={
|
||||
<div>
|
||||
<Typography.Text strong>
|
||||
{t('邀请链接')}:
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
<Typography.Text strong>{t('邀请链接')}:</Typography.Text>
|
||||
<Typography.Text
|
||||
link
|
||||
underline
|
||||
style={{marginLeft: 8}}
|
||||
onClick={() => window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')}
|
||||
underline
|
||||
style={{ marginLeft: 8 }}
|
||||
onClick={() =>
|
||||
window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')
|
||||
}
|
||||
>
|
||||
https://cloud.siliconflow.cn/i/hij0YNTZ
|
||||
</Typography.Text>
|
||||
@@ -482,27 +481,29 @@ const EditChannel = (props) => {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="AZURE_OPENAI_ENDPOINT"
|
||||
name="azure_base_url"
|
||||
placeholder={t('请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com')}
|
||||
label='AZURE_OPENAI_ENDPOINT'
|
||||
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"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>{t('默认 API 版本')}:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label={t('默认 API 版本')}
|
||||
name="azure_other"
|
||||
name='azure_other'
|
||||
placeholder={t('请输入默认 API 版本,例如:2024-12-01-preview')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -511,7 +512,9 @@ const EditChannel = (props) => {
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Banner
|
||||
type={'warning'}
|
||||
description={t('如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。')}
|
||||
description={t(
|
||||
'如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。',
|
||||
)}
|
||||
></Banner>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
@@ -520,13 +523,15 @@ const EditChannel = (props) => {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name="base_url"
|
||||
placeholder={t('请输入完整的URL,例如:https://api.openai.com/v1/chat/completions')}
|
||||
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"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -535,7 +540,9 @@ const EditChannel = (props) => {
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Banner
|
||||
type={'warning'}
|
||||
description={t('Dify渠道只适配chatflow和agent,并且agent不支持图片!')}
|
||||
description={t(
|
||||
'Dify渠道只适配chatflow和agent,并且agent不支持图片!',
|
||||
)}
|
||||
></Banner>
|
||||
</div>
|
||||
</>
|
||||
@@ -545,13 +552,13 @@ const EditChannel = (props) => {
|
||||
</div>
|
||||
<Input
|
||||
required
|
||||
name="name"
|
||||
name='name'
|
||||
placeholder={t('请为渠道命名')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('name', value);
|
||||
}}
|
||||
value={inputs.name}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && (
|
||||
<>
|
||||
@@ -578,7 +585,7 @@ const EditChannel = (props) => {
|
||||
{batch ? (
|
||||
<TextArea
|
||||
label={t('密钥')}
|
||||
name="key"
|
||||
name='key'
|
||||
required
|
||||
placeholder={t('请输入密钥,一行一个')}
|
||||
onChange={(value) => {
|
||||
@@ -586,16 +593,17 @@ const EditChannel = (props) => {
|
||||
}}
|
||||
value={inputs.key}
|
||||
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{inputs.type === 41 ? (
|
||||
<TextArea
|
||||
label={t('鉴权json')}
|
||||
name="key"
|
||||
name='key'
|
||||
required
|
||||
placeholder={'{\n' +
|
||||
placeholder={
|
||||
'{\n' +
|
||||
' "type": "service_account",\n' +
|
||||
' "project_id": "abc-bcd-123-456",\n' +
|
||||
' "private_key_id": "123xxxxx456",\n' +
|
||||
@@ -607,25 +615,26 @@ const EditChannel = (props) => {
|
||||
' "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"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
label={t('密钥')}
|
||||
name="key"
|
||||
name='key'
|
||||
required
|
||||
placeholder={t(type2secretPrompt(inputs.type))}
|
||||
onChange={(value) => {
|
||||
handleInputChange('key', value);
|
||||
}}
|
||||
value={inputs.key}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -636,7 +645,7 @@ const EditChannel = (props) => {
|
||||
<Checkbox
|
||||
checked={batch}
|
||||
label={t('批量创建')}
|
||||
name="batch"
|
||||
name='batch'
|
||||
onChange={() => setBatch(!batch)}
|
||||
/>
|
||||
<Typography.Text strong>{t('批量创建')}</Typography.Text>
|
||||
@@ -649,13 +658,15 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>{t('私有部署地址')}:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name="base_url"
|
||||
placeholder={t('请输入私有部署地址,格式为:https://fastgpt.run/api/openapi')}
|
||||
name='base_url'
|
||||
placeholder={t(
|
||||
'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi',
|
||||
)}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -663,17 +674,21 @@ const EditChannel = (props) => {
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
{t('注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用')}
|
||||
{t(
|
||||
'注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用',
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name="base_url"
|
||||
placeholder={t('请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com')}
|
||||
name='base_url'
|
||||
placeholder={t(
|
||||
'请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com',
|
||||
)}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -682,7 +697,7 @@ const EditChannel = (props) => {
|
||||
</div>
|
||||
<Select
|
||||
placeholder={t('请选择可以使用该渠道的分组')}
|
||||
name="groups"
|
||||
name='groups'
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
@@ -692,7 +707,7 @@ const EditChannel = (props) => {
|
||||
handleInputChange('groups', value);
|
||||
}}
|
||||
value={inputs.groups}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
optionList={groupOptions}
|
||||
/>
|
||||
{inputs.type === 18 && (
|
||||
@@ -701,7 +716,7 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>模型版本:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name="other"
|
||||
name='other'
|
||||
placeholder={
|
||||
'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
|
||||
}
|
||||
@@ -709,7 +724,7 @@ const EditChannel = (props) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -719,29 +734,31 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>{t('部署地区')}:</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
name="other"
|
||||
placeholder={t('请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
|
||||
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"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'other',
|
||||
JSON.stringify(REGION_EXAMPLE, null, 2)
|
||||
JSON.stringify(REGION_EXAMPLE, null, 2),
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -755,14 +772,14 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>知识库 ID:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="知识库 ID"
|
||||
name="other"
|
||||
label='知识库 ID'
|
||||
name='other'
|
||||
placeholder={'请输入知识库 ID,例如:123456'}
|
||||
onChange={(value) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -772,7 +789,7 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>Account ID:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name="other"
|
||||
name='other'
|
||||
placeholder={
|
||||
'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'
|
||||
}
|
||||
@@ -780,7 +797,7 @@ const EditChannel = (props) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -789,7 +806,7 @@ const EditChannel = (props) => {
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择该渠道所支持的模型'}
|
||||
name="models"
|
||||
name='models'
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
@@ -799,13 +816,13 @@ const EditChannel = (props) => {
|
||||
handleInputChange('models', value);
|
||||
}}
|
||||
value={inputs.models}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
optionList={modelOptions}
|
||||
/>
|
||||
<div style={{ lineHeight: '40px', marginBottom: '12px' }}>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
type='primary'
|
||||
onClick={() => {
|
||||
handleInputChange('models', basicModels);
|
||||
}}
|
||||
@@ -813,16 +830,20 @@ const EditChannel = (props) => {
|
||||
{t('填入相关模型')}
|
||||
</Button>
|
||||
<Button
|
||||
type="secondary"
|
||||
type='secondary'
|
||||
onClick={() => {
|
||||
handleInputChange('models', fullModels);
|
||||
}}
|
||||
>
|
||||
{t('填入所有模型')}
|
||||
</Button>
|
||||
<Tooltip content={t('新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出')}>
|
||||
<Tooltip
|
||||
content={t(
|
||||
'新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出',
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
type="tertiary"
|
||||
type='tertiary'
|
||||
onClick={() => {
|
||||
fetchUpstreamModelList('models');
|
||||
}}
|
||||
@@ -831,7 +852,7 @@ const EditChannel = (props) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type="warning"
|
||||
type='warning'
|
||||
onClick={() => {
|
||||
handleInputChange('models', []);
|
||||
}}
|
||||
@@ -841,7 +862,7 @@ const EditChannel = (props) => {
|
||||
</Space>
|
||||
<Input
|
||||
addonAfter={
|
||||
<Button type="primary" onClick={addCustomModels}>
|
||||
<Button type='primary' onClick={addCustomModels}>
|
||||
{t('填入')}
|
||||
</Button>
|
||||
}
|
||||
@@ -856,53 +877,53 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>{t('模型重定向')}:</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') + `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
|
||||
name="model_mapping"
|
||||
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"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'model_mapping',
|
||||
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
|
||||
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('填入模板')}
|
||||
</Typography.Text>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
{t('渠道标签')}
|
||||
</Typography.Text>
|
||||
<Typography.Text strong>{t('渠道标签')}</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label={t('渠道标签')}
|
||||
name="tag"
|
||||
name='tag'
|
||||
placeholder={t('渠道标签')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('tag', value);
|
||||
}}
|
||||
value={inputs.tag}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
{t('渠道优先级')}
|
||||
</Typography.Text>
|
||||
<Typography.Text strong>{t('渠道优先级')}</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label={t('渠道优先级')}
|
||||
name="priority"
|
||||
name='priority'
|
||||
placeholder={t('渠道优先级')}
|
||||
onChange={(value) => {
|
||||
const number = parseInt(value);
|
||||
@@ -913,16 +934,14 @@ const EditChannel = (props) => {
|
||||
}
|
||||
}}
|
||||
value={inputs.priority}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
{t('渠道权重')}
|
||||
</Typography.Text>
|
||||
<Typography.Text strong>{t('渠道权重')}</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label={t('渠道权重')}
|
||||
name="weight"
|
||||
name='weight'
|
||||
placeholder={t('渠道权重')}
|
||||
onChange={(value) => {
|
||||
const number = parseInt(value);
|
||||
@@ -933,37 +952,43 @@ const EditChannel = (props) => {
|
||||
}
|
||||
}}
|
||||
value={inputs.weight}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
{t('渠道额外设置')}:
|
||||
</Typography.Text>
|
||||
<Typography.Text strong>{t('渠道额外设置')}:</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={t('此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:') + '\n{\n "force_format": true\n}'}
|
||||
name="setting"
|
||||
placeholder={
|
||||
t(
|
||||
'此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:',
|
||||
) + '\n{\n "force_format": true\n}'
|
||||
}
|
||||
name='setting'
|
||||
onChange={(value) => {
|
||||
handleInputChange('setting', value);
|
||||
}}
|
||||
autosize
|
||||
value={inputs.setting}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
<Space>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'setting',
|
||||
JSON.stringify({
|
||||
force_format: true
|
||||
}, null, 2)
|
||||
JSON.stringify(
|
||||
{
|
||||
force_format: true,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -973,10 +998,12 @@ const EditChannel = (props) => {
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
window.open('https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md');
|
||||
window.open(
|
||||
'https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md',
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('设置说明')}
|
||||
@@ -985,19 +1012,21 @@ const EditChannel = (props) => {
|
||||
</>
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
{t('参数覆盖')}:
|
||||
</Typography.Text>
|
||||
<Typography.Text strong>{t('参数覆盖')}:</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:') + '\n{\n "temperature": 0\n}'}
|
||||
name="setting"
|
||||
placeholder={
|
||||
t(
|
||||
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:',
|
||||
) + '\n{\n "temperature": 0\n}'
|
||||
}
|
||||
name='setting'
|
||||
onChange={(value) => {
|
||||
handleInputChange('param_override', value);
|
||||
}}
|
||||
autosize
|
||||
value={inputs.param_override}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</>
|
||||
{inputs.type === 1 && (
|
||||
@@ -1007,7 +1036,7 @@ const EditChannel = (props) => {
|
||||
</div>
|
||||
<Input
|
||||
label={t('组织,可选,不填则为默认组织')}
|
||||
name="openai_organization"
|
||||
name='openai_organization'
|
||||
placeholder={t('请输入组织org-xxx')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('openai_organization', value);
|
||||
@@ -1020,7 +1049,7 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>{t('默认测试模型')}:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name="test_model"
|
||||
name='test_model'
|
||||
placeholder={t('不填则为模型列表第一个')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('test_model', value);
|
||||
@@ -1030,14 +1059,16 @@ const EditChannel = (props) => {
|
||||
<div style={{ marginTop: 10, display: 'flex' }}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
name="auto_ban"
|
||||
name='auto_ban'
|
||||
checked={autoBan}
|
||||
onChange={() => {
|
||||
setAutoBan(!autoBan);
|
||||
}}
|
||||
/>
|
||||
<Typography.Text strong>
|
||||
{t('是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:')}
|
||||
{t(
|
||||
'是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:',
|
||||
)}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -1047,26 +1078,31 @@ const EditChannel = (props) => {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={t('此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:') +
|
||||
'\n' + JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}
|
||||
name="status_code_mapping"
|
||||
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"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'status_code_mapping',
|
||||
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
|
||||
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2),
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { API, showError, showInfo, showSuccess, showWarning, verifyJSON } from '../../helpers';
|
||||
import { SideSheet, Space, Button, Input, Typography, Spin, Modal, Select, Banner, TextArea } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
API,
|
||||
showError,
|
||||
showInfo,
|
||||
showSuccess,
|
||||
showWarning,
|
||||
verifyJSON,
|
||||
} from '../../helpers';
|
||||
import {
|
||||
SideSheet,
|
||||
Space,
|
||||
Button,
|
||||
Input,
|
||||
Typography,
|
||||
Spin,
|
||||
Modal,
|
||||
Select,
|
||||
Banner,
|
||||
TextArea,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import TextInput from '../../components/custom/TextInput.js';
|
||||
import { getChannelModels } from '../../components/utils.js';
|
||||
|
||||
const MODEL_MAPPING_EXAMPLE = {
|
||||
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
|
||||
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
|
||||
};
|
||||
|
||||
const EditTagModal = (props) => {
|
||||
@@ -23,7 +41,7 @@ const EditTagModal = (props) => {
|
||||
model_mapping: null,
|
||||
groups: [],
|
||||
models: [],
|
||||
}
|
||||
};
|
||||
const [inputs, setInputs] = useState(originInputs);
|
||||
|
||||
const handleInputChange = (name, value) => {
|
||||
@@ -39,7 +57,7 @@ const EditTagModal = (props) => {
|
||||
'mj_blend',
|
||||
'mj_upscale',
|
||||
'mj_describe',
|
||||
'mj_uploads'
|
||||
'mj_uploads',
|
||||
];
|
||||
break;
|
||||
case 5:
|
||||
@@ -59,14 +77,11 @@ const EditTagModal = (props) => {
|
||||
'mj_high_variation',
|
||||
'mj_low_variation',
|
||||
'mj_pan',
|
||||
'mj_uploads'
|
||||
'mj_uploads',
|
||||
];
|
||||
break;
|
||||
case 36:
|
||||
localModels = [
|
||||
'suno_music',
|
||||
'suno_lyrics'
|
||||
];
|
||||
localModels = ['suno_music', 'suno_lyrics'];
|
||||
break;
|
||||
default:
|
||||
localModels = getChannelModels(value);
|
||||
@@ -84,7 +99,7 @@ const EditTagModal = (props) => {
|
||||
let res = await API.get(`/api/channel/models`);
|
||||
let localModelOptions = res.data.data.map((model) => ({
|
||||
label: model.id,
|
||||
value: model.id
|
||||
value: model.id,
|
||||
}));
|
||||
setOriginModelOptions(localModelOptions);
|
||||
setFullModels(res.data.data.map((model) => model.id));
|
||||
@@ -93,7 +108,7 @@ const EditTagModal = (props) => {
|
||||
.filter((model) => {
|
||||
return model.id.startsWith('gpt-') || model.id.startsWith('text-');
|
||||
})
|
||||
.map((model) => model.id)
|
||||
.map((model) => model.id),
|
||||
);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
@@ -109,27 +124,26 @@ const EditTagModal = (props) => {
|
||||
setGroupOptions(
|
||||
res.data.data.map((group) => ({
|
||||
label: group,
|
||||
value: group
|
||||
}))
|
||||
value: group,
|
||||
})),
|
||||
);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
let data = {
|
||||
tag: tag,
|
||||
}
|
||||
};
|
||||
if (inputs.model_mapping !== null && inputs.model_mapping !== '') {
|
||||
if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
|
||||
showInfo('模型映射必须是合法的 JSON 格式!');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
data.model_mapping = inputs.model_mapping
|
||||
data.model_mapping = inputs.model_mapping;
|
||||
}
|
||||
if (inputs.groups.length > 0) {
|
||||
data.groups = inputs.groups.join(',');
|
||||
@@ -139,7 +153,12 @@ const EditTagModal = (props) => {
|
||||
}
|
||||
data.new_tag = inputs.new_tag;
|
||||
// check have any change
|
||||
if (data.model_mapping === undefined && data.groups === undefined && data.models === undefined && data.new_tag === undefined) {
|
||||
if (
|
||||
data.model_mapping === undefined &&
|
||||
data.groups === undefined &&
|
||||
data.models === undefined &&
|
||||
data.new_tag === undefined
|
||||
) {
|
||||
showWarning('没有任何修改!');
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -159,7 +178,7 @@ const EditTagModal = (props) => {
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let localModelOptions = [...originModelOptions];
|
||||
@@ -167,7 +186,7 @@ const EditTagModal = (props) => {
|
||||
if (!localModelOptions.find((option) => option.label === model)) {
|
||||
localModelOptions.push({
|
||||
label: model,
|
||||
value: model
|
||||
value: model,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -179,7 +198,7 @@ const EditTagModal = (props) => {
|
||||
...originInputs,
|
||||
tag: tag,
|
||||
new_tag: tag,
|
||||
})
|
||||
});
|
||||
fetchModels().then();
|
||||
fetchGroups().then();
|
||||
}, [visible]);
|
||||
@@ -201,7 +220,7 @@ const EditTagModal = (props) => {
|
||||
// 添加到下拉选项
|
||||
key: model,
|
||||
text: model,
|
||||
value: model
|
||||
value: model,
|
||||
});
|
||||
} else if (model) {
|
||||
showError('某些模型已存在!');
|
||||
@@ -217,17 +236,18 @@ const EditTagModal = (props) => {
|
||||
handleInputChange('models', localModels);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<SideSheet
|
||||
title="编辑标签"
|
||||
title='编辑标签'
|
||||
visible={visible}
|
||||
onCancel={handleClose}
|
||||
footer={
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Space>
|
||||
<Button onClick={handleClose}>取消</Button>
|
||||
<Button type="primary" onClick={handleSave} loading={loading}>保存</Button>
|
||||
<Button type='primary' onClick={handleSave} loading={loading}>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
@@ -235,27 +255,23 @@ const EditTagModal = (props) => {
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Banner
|
||||
type={'warning'}
|
||||
description={
|
||||
<>
|
||||
所有编辑均为覆盖操作,留空则不更改
|
||||
</>
|
||||
}
|
||||
description={<>所有编辑均为覆盖操作,留空则不更改</>}
|
||||
></Banner>
|
||||
</div>
|
||||
<Spin spinning={loading}>
|
||||
<TextInput
|
||||
label="标签名,留空则解散标签"
|
||||
name="newTag"
|
||||
label='标签名,留空则解散标签'
|
||||
name='newTag'
|
||||
value={inputs.new_tag}
|
||||
onChange={(value) => setInputs({ ...inputs, new_tag: value })}
|
||||
placeholder="请输入新标签"
|
||||
placeholder='请输入新标签'
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>模型,留空则不更改:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择该渠道所支持的模型,留空则不更改'}
|
||||
name="models"
|
||||
name='models'
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
@@ -265,16 +281,16 @@ const EditTagModal = (props) => {
|
||||
handleInputChange('models', value);
|
||||
}}
|
||||
value={inputs.models}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
optionList={modelOptions}
|
||||
/>
|
||||
<Input
|
||||
addonAfter={
|
||||
<Button type="primary" onClick={addCustomModels}>
|
||||
<Button type='primary' onClick={addCustomModels}>
|
||||
填入
|
||||
</Button>
|
||||
}
|
||||
placeholder="输入自定义模型名称"
|
||||
placeholder='输入自定义模型名称'
|
||||
value={customModel}
|
||||
onChange={(value) => {
|
||||
setCustomModel(value.trim());
|
||||
@@ -285,7 +301,7 @@ const EditTagModal = (props) => {
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择可以使用该渠道的分组,留空则不更改'}
|
||||
name="groups"
|
||||
name='groups'
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
@@ -295,7 +311,7 @@ const EditTagModal = (props) => {
|
||||
handleInputChange('groups', value);
|
||||
}}
|
||||
value={inputs.groups}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
optionList={groupOptions}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
@@ -303,25 +319,25 @@ const EditTagModal = (props) => {
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改`}
|
||||
name="model_mapping"
|
||||
name='model_mapping'
|
||||
onChange={(value) => {
|
||||
handleInputChange('model_mapping', value);
|
||||
}}
|
||||
autosize
|
||||
value={inputs.model_mapping}
|
||||
autoComplete="new-password"
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
<Space>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'model_mapping',
|
||||
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
|
||||
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2),
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -331,13 +347,10 @@ const EditTagModal = (props) => {
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'model_mapping',
|
||||
JSON.stringify({}, null, 2)
|
||||
);
|
||||
handleInputChange('model_mapping', JSON.stringify({}, null, 2));
|
||||
}}
|
||||
>
|
||||
清空重定向
|
||||
@@ -346,13 +359,10 @@ const EditTagModal = (props) => {
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'model_mapping',
|
||||
""
|
||||
);
|
||||
handleInputChange('model_mapping', '');
|
||||
}}
|
||||
>
|
||||
不更改
|
||||
@@ -363,4 +373,4 @@ const EditTagModal = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default EditTagModal;
|
||||
export default EditTagModal;
|
||||
|
||||
@@ -9,10 +9,10 @@ const File = () => {
|
||||
<>
|
||||
<Layout>
|
||||
<Layout.Header>
|
||||
<h3>{t('管理渠道')}</h3>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<ChannelsTable />
|
||||
<h3>{t('管理渠道')}</h3>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<ChannelsTable />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTokenKeys } from '../../components/fetchTokenKeys';
|
||||
import {Banner, Layout} from '@douyinfe/semi-ui';
|
||||
import { Banner, Layout } from '@douyinfe/semi-ui';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const ChatPage = () => {
|
||||
@@ -10,21 +10,24 @@ const ChatPage = () => {
|
||||
const comLink = (key) => {
|
||||
// console.log('chatLink:', chatLink);
|
||||
if (!serverAddress || !key) return '';
|
||||
let link = "";
|
||||
if (id) {
|
||||
let chats = localStorage.getItem('chats');
|
||||
if (chats) {
|
||||
chats = JSON.parse(chats);
|
||||
if (Array.isArray(chats) && chats.length > 0) {
|
||||
for (let k in chats[id]) {
|
||||
link = chats[id][k];
|
||||
link = link.replaceAll('{address}', encodeURIComponent(serverAddress));
|
||||
link = link.replaceAll('{key}', 'sk-' + key);
|
||||
}
|
||||
}
|
||||
let link = '';
|
||||
if (id) {
|
||||
let chats = localStorage.getItem('chats');
|
||||
if (chats) {
|
||||
chats = JSON.parse(chats);
|
||||
if (Array.isArray(chats) && chats.length > 0) {
|
||||
for (let k in chats[id]) {
|
||||
link = chats[id][k];
|
||||
link = link.replaceAll(
|
||||
'{address}',
|
||||
encodeURIComponent(serverAddress),
|
||||
);
|
||||
link = link.replaceAll('{key}', 'sk-' + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return link;
|
||||
}
|
||||
return link;
|
||||
};
|
||||
|
||||
const iframeSrc = keys.length > 0 ? comLink(keys[0]) : '';
|
||||
@@ -33,21 +36,18 @@ const ChatPage = () => {
|
||||
<iframe
|
||||
src={iframeSrc}
|
||||
style={{ width: '100%', height: '100%', border: 'none' }}
|
||||
title="Token Frame"
|
||||
allow="camera;microphone"
|
||||
title='Token Frame'
|
||||
allow='camera;microphone'
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<Layout>
|
||||
<Layout.Header>
|
||||
<Banner
|
||||
description={"正在跳转......"}
|
||||
type={"warning"}
|
||||
/>
|
||||
<Banner description={'正在跳转......'} type={'warning'} />
|
||||
</Layout.Header>
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatPage;
|
||||
export default ChatPage;
|
||||
|
||||
@@ -18,9 +18,9 @@ const chat2page = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>正在加载,请稍候...</h3>
|
||||
<h3>正在加载,请稍候...</h3>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default chat2page;
|
||||
export default chat2page;
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
|
||||
|
||||
import { Button, Card, Col, Descriptions, Form, Layout, Row, Spin, Tabs } from '@douyinfe/semi-ui';
|
||||
import { VChart } from "@visactor/react-vchart";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Descriptions,
|
||||
Form,
|
||||
Layout,
|
||||
Row,
|
||||
Spin,
|
||||
Tabs,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { VChart } from '@visactor/react-vchart';
|
||||
import {
|
||||
API,
|
||||
isAdmin,
|
||||
@@ -59,10 +69,12 @@ const Detail = (props) => {
|
||||
const [lineData, setLineData] = useState([]);
|
||||
const [spec_pie, setSpecPie] = useState({
|
||||
type: 'pie',
|
||||
data: [{
|
||||
id: 'id0',
|
||||
values: pieData
|
||||
}],
|
||||
data: [
|
||||
{
|
||||
id: 'id0',
|
||||
values: pieData,
|
||||
},
|
||||
],
|
||||
outerRadius: 0.8,
|
||||
innerRadius: 0.5,
|
||||
padAngle: 0.6,
|
||||
@@ -113,10 +125,12 @@ const Detail = (props) => {
|
||||
});
|
||||
const [spec_line, setSpecLine] = useState({
|
||||
type: 'bar',
|
||||
data: [{
|
||||
id: 'barData',
|
||||
values: lineData
|
||||
}],
|
||||
data: [
|
||||
{
|
||||
id: 'barData',
|
||||
values: lineData,
|
||||
},
|
||||
],
|
||||
xField: 'Time',
|
||||
yField: 'Usage',
|
||||
seriesField: 'Model',
|
||||
@@ -158,7 +172,7 @@ const Detail = (props) => {
|
||||
array.sort((a, b) => b.value - a.value);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i].key == "其他") {
|
||||
if (array[i].key == '其他') {
|
||||
continue;
|
||||
}
|
||||
let value = parseFloat(array[i].value);
|
||||
@@ -245,7 +259,7 @@ const Detail = (props) => {
|
||||
let totalTokens = 0;
|
||||
|
||||
// 收集所有唯一的模型名称
|
||||
data.forEach(item => {
|
||||
data.forEach((item) => {
|
||||
uniqueModels.add(item.model_name);
|
||||
totalTokens += item.token_used;
|
||||
totalQuota += item.quota;
|
||||
@@ -255,15 +269,16 @@ const Detail = (props) => {
|
||||
// 处理颜色映射
|
||||
const newModelColors = {};
|
||||
Array.from(uniqueModels).forEach((modelName) => {
|
||||
newModelColors[modelName] = modelColorMap[modelName] ||
|
||||
modelColors[modelName] ||
|
||||
newModelColors[modelName] =
|
||||
modelColorMap[modelName] ||
|
||||
modelColors[modelName] ||
|
||||
modelToColor(modelName);
|
||||
});
|
||||
setModelColors(newModelColors);
|
||||
|
||||
// 按时间和模型聚合数据
|
||||
let aggregatedData = new Map();
|
||||
data.forEach(item => {
|
||||
data.forEach((item) => {
|
||||
const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime);
|
||||
const modelKey = item.model_name;
|
||||
const key = `${timeKey}-${modelKey}`;
|
||||
@@ -273,10 +288,10 @@ const Detail = (props) => {
|
||||
time: timeKey,
|
||||
model: modelKey,
|
||||
quota: 0,
|
||||
count: 0
|
||||
count: 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const existing = aggregatedData.get(key);
|
||||
existing.quota += item.quota;
|
||||
existing.count += item.count;
|
||||
@@ -293,48 +308,53 @@ const Detail = (props) => {
|
||||
|
||||
newPieData = Array.from(modelTotals).map(([model, count]) => ({
|
||||
type: model,
|
||||
value: count
|
||||
value: count,
|
||||
}));
|
||||
|
||||
// 生成时间点序列
|
||||
let timePoints = Array.from(new Set([...aggregatedData.values()].map(d => d.time)));
|
||||
let timePoints = Array.from(
|
||||
new Set([...aggregatedData.values()].map((d) => d.time)),
|
||||
);
|
||||
if (timePoints.length < 7) {
|
||||
const lastTime = Math.max(...data.map(item => item.created_at));
|
||||
const interval = dataExportDefaultTime === 'hour' ? 3600
|
||||
: dataExportDefaultTime === 'day' ? 86400
|
||||
: 604800;
|
||||
|
||||
timePoints = Array.from({length: 7}, (_, i) =>
|
||||
timestamp2string1(lastTime - (6-i) * interval, dataExportDefaultTime)
|
||||
const lastTime = Math.max(...data.map((item) => item.created_at));
|
||||
const interval =
|
||||
dataExportDefaultTime === 'hour'
|
||||
? 3600
|
||||
: dataExportDefaultTime === 'day'
|
||||
? 86400
|
||||
: 604800;
|
||||
|
||||
timePoints = Array.from({ length: 7 }, (_, i) =>
|
||||
timestamp2string1(lastTime - (6 - i) * interval, dataExportDefaultTime),
|
||||
);
|
||||
}
|
||||
|
||||
// 生成柱状图数据
|
||||
timePoints.forEach(time => {
|
||||
timePoints.forEach((time) => {
|
||||
// 为每个时间点收集所有模型的数据
|
||||
let timeData = Array.from(uniqueModels).map(model => {
|
||||
let timeData = Array.from(uniqueModels).map((model) => {
|
||||
const key = `${time}-${model}`;
|
||||
const aggregated = aggregatedData.get(key);
|
||||
return {
|
||||
Time: time,
|
||||
Model: model,
|
||||
rawQuota: aggregated?.quota || 0,
|
||||
Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0
|
||||
Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// 计算该时间点的总计
|
||||
const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0);
|
||||
|
||||
|
||||
// 按照 rawQuota 从大到小排序
|
||||
timeData.sort((a, b) => b.rawQuota - a.rawQuota);
|
||||
|
||||
|
||||
// 为每个数据点添加该时间的总计
|
||||
timeData = timeData.map(item => ({
|
||||
timeData = timeData.map((item) => ({
|
||||
...item,
|
||||
TimeSum: timeSum
|
||||
TimeSum: timeSum,
|
||||
}));
|
||||
|
||||
|
||||
// 将排序后的数据添加到 newLineData
|
||||
newLineData.push(...timeData);
|
||||
});
|
||||
@@ -344,30 +364,30 @@ const Detail = (props) => {
|
||||
newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
|
||||
|
||||
// 更新图表配置和数据
|
||||
setSpecPie(prev => ({
|
||||
setSpecPie((prev) => ({
|
||||
...prev,
|
||||
data: [{ id: 'id0', values: newPieData }],
|
||||
title: {
|
||||
...prev.title,
|
||||
subtext: `${t('总计')}:${renderNumber(totalTimes)}`
|
||||
subtext: `${t('总计')}:${renderNumber(totalTimes)}`,
|
||||
},
|
||||
color: {
|
||||
specified: newModelColors
|
||||
}
|
||||
specified: newModelColors,
|
||||
},
|
||||
}));
|
||||
|
||||
setSpecLine(prev => ({
|
||||
setSpecLine((prev) => ({
|
||||
...prev,
|
||||
data: [{ id: 'barData', values: newLineData }],
|
||||
title: {
|
||||
...prev.title,
|
||||
subtext: `${t('总计')}:${renderQuota(totalQuota, 2)}`
|
||||
subtext: `${t('总计')}:${renderQuota(totalQuota, 2)}`,
|
||||
},
|
||||
color: {
|
||||
specified: newModelColors
|
||||
}
|
||||
specified: newModelColors,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
setPieData(newPieData);
|
||||
setLineData(newLineData);
|
||||
setConsumeQuota(totalQuota);
|
||||
@@ -377,16 +397,16 @@ const Detail = (props) => {
|
||||
|
||||
const getUserData = async () => {
|
||||
let res = await API.get(`/api/user/self`);
|
||||
const {success, message, data} = res.data;
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
userDispatch({type: 'login', payload: data});
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getUserData()
|
||||
getUserData();
|
||||
if (!initialized.current) {
|
||||
initVChartSemiTheme({
|
||||
isWatchingThemeSwitch: true,
|
||||
@@ -468,15 +488,19 @@ const Detail = (props) => {
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Form.Section>
|
||||
</Form.Section>
|
||||
<Form.Section></Form.Section>
|
||||
</>
|
||||
</Form>
|
||||
<Spin spinning={loading}>
|
||||
<Row gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }} style={{marginTop: 20}} type="flex" justify="space-between">
|
||||
<Col span={styleState.isMobile?24:8}>
|
||||
<Row
|
||||
gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }}
|
||||
style={{ marginTop: 20 }}
|
||||
type='flex'
|
||||
justify='space-between'
|
||||
>
|
||||
<Col span={styleState.isMobile ? 24 : 8}>
|
||||
<Card className='panel-desc-card'>
|
||||
<Descriptions row size="small">
|
||||
<Descriptions row size='small'>
|
||||
<Descriptions.Item itemKey={t('当前余额')}>
|
||||
{renderQuota(userState?.user?.quota)}
|
||||
</Descriptions.Item>
|
||||
@@ -489,9 +513,9 @@ const Detail = (props) => {
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={styleState.isMobile?24:8}>
|
||||
<Col span={styleState.isMobile ? 24 : 8}>
|
||||
<Card>
|
||||
<Descriptions row size="small">
|
||||
<Descriptions row size='small'>
|
||||
<Descriptions.Item itemKey={t('统计额度')}>
|
||||
{renderQuota(consumeQuota)}
|
||||
</Descriptions.Item>
|
||||
@@ -508,40 +532,43 @@ const Detail = (props) => {
|
||||
<Card>
|
||||
<Descriptions row size='small'>
|
||||
<Descriptions.Item itemKey={t('平均RPM')}>
|
||||
{(times /
|
||||
{(
|
||||
times /
|
||||
((Date.parse(end_timestamp) -
|
||||
Date.parse(start_timestamp)) /
|
||||
60000)).toFixed(3)}
|
||||
60000)
|
||||
).toFixed(3)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey={t('平均TPM')}>
|
||||
{(consumeTokens /
|
||||
{(
|
||||
consumeTokens /
|
||||
((Date.parse(end_timestamp) -
|
||||
Date.parse(start_timestamp)) /
|
||||
60000)).toFixed(3)}
|
||||
60000)
|
||||
).toFixed(3)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Card style={{marginTop: 20}}>
|
||||
<Tabs type="line" defaultActiveKey="1">
|
||||
<Tabs.TabPane tab={t('消耗分布')} itemKey="1">
|
||||
<Card style={{ marginTop: 20 }}>
|
||||
<Tabs type='line' defaultActiveKey='1'>
|
||||
<Tabs.TabPane tab={t('消耗分布')} itemKey='1'>
|
||||
<div style={{ height: 500 }}>
|
||||
<VChart
|
||||
spec={spec_line}
|
||||
option={{ mode: "desktop-browser" }}
|
||||
option={{ mode: 'desktop-browser' }}
|
||||
/>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t('调用次数分布')} itemKey="2">
|
||||
<Tabs.TabPane tab={t('调用次数分布')} itemKey='2'>
|
||||
<div style={{ height: 500 }}>
|
||||
<VChart
|
||||
spec={spec_pie}
|
||||
option={{ mode: "desktop-browser" }}
|
||||
option={{ mode: 'desktop-browser' }}
|
||||
/>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Spin>
|
||||
|
||||
@@ -40,19 +40,19 @@ const Home = () => {
|
||||
setHomePageContent(content);
|
||||
localStorage.setItem('home_page_content', content);
|
||||
|
||||
// 如果内容是 URL,则发送主题模式
|
||||
if (data.startsWith('https://')) {
|
||||
const iframe = document.querySelector('iframe');
|
||||
if (iframe) {
|
||||
const theme = localStorage.getItem('theme-mode') || 'light';
|
||||
// 测试是否正确传递theme-mode给iframe
|
||||
// console.log('Sending theme-mode to iframe:', theme);
|
||||
iframe.onload = () => {
|
||||
iframe.contentWindow.postMessage({ themeMode: theme }, '*');
|
||||
iframe.contentWindow.postMessage({ lang: i18n.language }, '*');
|
||||
};
|
||||
}
|
||||
// 如果内容是 URL,则发送主题模式
|
||||
if (data.startsWith('https://')) {
|
||||
const iframe = document.querySelector('iframe');
|
||||
if (iframe) {
|
||||
const theme = localStorage.getItem('theme-mode') || 'light';
|
||||
// 测试是否正确传递theme-mode给iframe
|
||||
// console.log('Sending theme-mode to iframe:', theme);
|
||||
iframe.onload = () => {
|
||||
iframe.contentWindow.postMessage({ themeMode: theme }, '*');
|
||||
iframe.contentWindow.postMessage({ lang: i18n.language }, '*');
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
setHomePageContent('加载首页内容失败...');
|
||||
@@ -95,7 +95,9 @@ const Home = () => {
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<p>{t('名称')}:{statusState?.status?.system_name}</p>
|
||||
<p>
|
||||
{t('名称')}:{statusState?.status?.system_name}
|
||||
</p>
|
||||
<p>
|
||||
{t('版本')}:
|
||||
{statusState?.status?.version
|
||||
@@ -123,7 +125,9 @@ const Home = () => {
|
||||
Apache-2.0 License
|
||||
</a>
|
||||
</p>
|
||||
<p>{t('启动时间')}:{getStartTimeString()}</p>
|
||||
<p>
|
||||
{t('启动时间')}:{getStartTimeString()}
|
||||
</p>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
@@ -155,8 +159,8 @@ const Home = () => {
|
||||
<p>
|
||||
{t('OIDC 身份验证')}:
|
||||
{statusState?.status?.oidc === true
|
||||
? t('已启用')
|
||||
: t('未启用')}
|
||||
? t('已启用')
|
||||
: t('未启用')}
|
||||
</p>
|
||||
<p>
|
||||
{t('微信身份验证')}:
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { API, getUserIdFromLocalStorage, showError } from '../../helpers/index.js';
|
||||
import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography, Button, Highlight } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
API,
|
||||
getUserIdFromLocalStorage,
|
||||
showError,
|
||||
} from '../../helpers/index.js';
|
||||
import {
|
||||
Card,
|
||||
Chat,
|
||||
Input,
|
||||
Layout,
|
||||
Select,
|
||||
Slider,
|
||||
TextArea,
|
||||
Typography,
|
||||
Button,
|
||||
Highlight,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { SSE } from 'sse';
|
||||
import { IconSetting } from '@douyinfe/semi-icons';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
@@ -12,26 +27,28 @@ import { renderGroupOption, truncateText } from '../../helpers/render.js';
|
||||
const roleInfo = {
|
||||
user: {
|
||||
name: 'User',
|
||||
avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
|
||||
avatar:
|
||||
'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
|
||||
},
|
||||
assistant: {
|
||||
name: 'Assistant',
|
||||
avatar: 'logo.png'
|
||||
avatar: 'logo.png',
|
||||
},
|
||||
system: {
|
||||
name: 'System',
|
||||
avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
|
||||
}
|
||||
}
|
||||
avatar:
|
||||
'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
|
||||
},
|
||||
};
|
||||
|
||||
let id = 4;
|
||||
function getId() {
|
||||
return `${id++}`
|
||||
return `${id++}`;
|
||||
}
|
||||
|
||||
const Playground = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const defaultMessage = [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -44,7 +61,7 @@ const Playground = () => {
|
||||
id: '3',
|
||||
createAt: 1715676751919,
|
||||
content: t('你好,请问有什么可以帮助您的吗?'),
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -56,7 +73,9 @@ const Playground = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [status, setStatus] = useState({});
|
||||
const [systemPrompt, setSystemPrompt] = useState('You are a helpful assistant. You can help me by answering my questions. You can also ask me questions.');
|
||||
const [systemPrompt, setSystemPrompt] = useState(
|
||||
'You are a helpful assistant. You can help me by answering my questions. You can also ask me questions.',
|
||||
);
|
||||
const [message, setMessage] = useState(defaultMessage);
|
||||
const [models, setModels] = useState([]);
|
||||
const [groups, setGroups] = useState([]);
|
||||
@@ -99,26 +118,35 @@ const Playground = () => {
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let localGroupOptions = Object.entries(data).map(([group, info]) => ({
|
||||
label: truncateText(info.desc, "50%"),
|
||||
label: truncateText(info.desc, '50%'),
|
||||
value: group,
|
||||
ratio: info.ratio,
|
||||
fullLabel: info.desc // 保存完整文本用于tooltip
|
||||
fullLabel: info.desc, // 保存完整文本用于tooltip
|
||||
}));
|
||||
|
||||
if (localGroupOptions.length === 0) {
|
||||
localGroupOptions = [{
|
||||
label: t('用户分组'),
|
||||
value: '',
|
||||
ratio: 1
|
||||
}];
|
||||
localGroupOptions = [
|
||||
{
|
||||
label: t('用户分组'),
|
||||
value: '',
|
||||
ratio: 1,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
const localUser = JSON.parse(localStorage.getItem('user'));
|
||||
const userGroup = (userState.user && userState.user.group) || (localUser && localUser.group);
|
||||
|
||||
const userGroup =
|
||||
(userState.user && userState.user.group) ||
|
||||
(localUser && localUser.group);
|
||||
|
||||
if (userGroup) {
|
||||
const userGroupIndex = localGroupOptions.findIndex(g => g.value === userGroup);
|
||||
const userGroupIndex = localGroupOptions.findIndex(
|
||||
(g) => g.value === userGroup,
|
||||
);
|
||||
if (userGroupIndex > -1) {
|
||||
const userGroupOption = localGroupOptions.splice(userGroupIndex, 1)[0];
|
||||
const userGroupOption = localGroupOptions.splice(
|
||||
userGroupIndex,
|
||||
1,
|
||||
)[0];
|
||||
localGroupOptions.unshift(userGroupOption);
|
||||
}
|
||||
}
|
||||
@@ -135,7 +163,7 @@ const Playground = () => {
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: '16px',
|
||||
margin: '0px 8px',
|
||||
}
|
||||
};
|
||||
|
||||
const getSystemMessage = () => {
|
||||
if (systemPrompt !== '') {
|
||||
@@ -144,22 +172,22 @@ const Playground = () => {
|
||||
id: '1',
|
||||
createAt: 1715676751919,
|
||||
content: systemPrompt,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let handleSSE = (payload) => {
|
||||
let source = new SSE('/pg/chat/completions', {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"New-Api-User": getUserIdFromLocalStorage(),
|
||||
'Content-Type': 'application/json',
|
||||
'New-Api-User': getUserIdFromLocalStorage(),
|
||||
},
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
payload: JSON.stringify(payload),
|
||||
});
|
||||
source.addEventListener("message", (e) => {
|
||||
source.addEventListener('message', (e) => {
|
||||
// 只有收到 [DONE] 时才结束
|
||||
if (e.data === "[DONE]") {
|
||||
if (e.data === '[DONE]') {
|
||||
source.close();
|
||||
completeMessage();
|
||||
return;
|
||||
@@ -172,12 +200,12 @@ const Playground = () => {
|
||||
}
|
||||
});
|
||||
|
||||
source.addEventListener("error", (e) => {
|
||||
generateMockResponse(e.data)
|
||||
completeMessage('error')
|
||||
source.addEventListener('error', (e) => {
|
||||
generateMockResponse(e.data);
|
||||
completeMessage('error');
|
||||
});
|
||||
|
||||
source.addEventListener("readystatechange", (e) => {
|
||||
source.addEventListener('readystatechange', (e) => {
|
||||
if (e.readyState >= 2) {
|
||||
if (source.status === undefined) {
|
||||
source.close();
|
||||
@@ -186,55 +214,58 @@ const Playground = () => {
|
||||
}
|
||||
});
|
||||
source.stream();
|
||||
}
|
||||
};
|
||||
|
||||
const onMessageSend = useCallback((content, attachment) => {
|
||||
console.log("attachment: ", attachment);
|
||||
setMessage((prevMessage) => {
|
||||
const newMessage = [
|
||||
...prevMessage,
|
||||
{
|
||||
role: 'user',
|
||||
content: content,
|
||||
createAt: Date.now(),
|
||||
id: getId()
|
||||
}
|
||||
];
|
||||
const onMessageSend = useCallback(
|
||||
(content, attachment) => {
|
||||
console.log('attachment: ', attachment);
|
||||
setMessage((prevMessage) => {
|
||||
const newMessage = [
|
||||
...prevMessage,
|
||||
{
|
||||
role: 'user',
|
||||
content: content,
|
||||
createAt: Date.now(),
|
||||
id: getId(),
|
||||
},
|
||||
];
|
||||
|
||||
// 将 getPayload 移到这里
|
||||
const getPayload = () => {
|
||||
let systemMessage = getSystemMessage();
|
||||
let messages = newMessage.map((item) => {
|
||||
return {
|
||||
role: item.role,
|
||||
content: item.content,
|
||||
// 将 getPayload 移到这里
|
||||
const getPayload = () => {
|
||||
let systemMessage = getSystemMessage();
|
||||
let messages = newMessage.map((item) => {
|
||||
return {
|
||||
role: item.role,
|
||||
content: item.content,
|
||||
};
|
||||
});
|
||||
if (systemMessage) {
|
||||
messages.unshift(systemMessage);
|
||||
}
|
||||
});
|
||||
if (systemMessage) {
|
||||
messages.unshift(systemMessage);
|
||||
}
|
||||
return {
|
||||
messages: messages,
|
||||
stream: true,
|
||||
model: inputs.model,
|
||||
group: inputs.group,
|
||||
max_tokens: parseInt(inputs.max_tokens),
|
||||
temperature: inputs.temperature,
|
||||
return {
|
||||
messages: messages,
|
||||
stream: true,
|
||||
model: inputs.model,
|
||||
group: inputs.group,
|
||||
max_tokens: parseInt(inputs.max_tokens),
|
||||
temperature: inputs.temperature,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// 使用更新后的消息状态调用 handleSSE
|
||||
handleSSE(getPayload());
|
||||
newMessage.push({
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
createAt: Date.now(),
|
||||
id: getId(),
|
||||
status: 'loading'
|
||||
// 使用更新后的消息状态调用 handleSSE
|
||||
handleSSE(getPayload());
|
||||
newMessage.push({
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
createAt: Date.now(),
|
||||
id: getId(),
|
||||
status: 'loading',
|
||||
});
|
||||
return newMessage;
|
||||
});
|
||||
return newMessage;
|
||||
});
|
||||
}, [getSystemMessage]);
|
||||
},
|
||||
[getSystemMessage],
|
||||
);
|
||||
|
||||
const completeMessage = useCallback((status = 'complete') => {
|
||||
// console.log("Complete Message: ", status)
|
||||
@@ -244,27 +275,27 @@ const Playground = () => {
|
||||
if (lastMessage.status === 'complete' || lastMessage.status === 'error') {
|
||||
return prevMessage;
|
||||
}
|
||||
return [
|
||||
...prevMessage.slice(0, -1),
|
||||
{ ...lastMessage, status: status }
|
||||
];
|
||||
return [...prevMessage.slice(0, -1), { ...lastMessage, status: status }];
|
||||
});
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const generateMockResponse = useCallback((content) => {
|
||||
// console.log("Generate Mock Response: ", content);
|
||||
setMessage((message) => {
|
||||
const lastMessage = message[message.length - 1];
|
||||
let newMessage = {...lastMessage};
|
||||
if (lastMessage.status === 'loading' || lastMessage.status === 'incomplete') {
|
||||
let newMessage = { ...lastMessage };
|
||||
if (
|
||||
lastMessage.status === 'loading' ||
|
||||
lastMessage.status === 'incomplete'
|
||||
) {
|
||||
newMessage = {
|
||||
...newMessage,
|
||||
content: (lastMessage.content || '') + content,
|
||||
status: 'incomplete'
|
||||
}
|
||||
status: 'incomplete',
|
||||
};
|
||||
}
|
||||
return [ ...message.slice(0, -1), newMessage ]
|
||||
})
|
||||
return [...message.slice(0, -1), newMessage];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const SettingsToggle = () => {
|
||||
@@ -285,34 +316,47 @@ const Playground = () => {
|
||||
boxShadow: '2px 0 8px rgba(0, 0, 0, 0.15)',
|
||||
}}
|
||||
onClick={() => setShowSettings(!showSettings)}
|
||||
theme="solid"
|
||||
type="primary"
|
||||
theme='solid'
|
||||
type='primary'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function CustomInputRender(props) {
|
||||
const { detailProps } = props;
|
||||
const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = detailProps;
|
||||
const { clearContextNode, uploadNode, inputNode, sendNode, onClick } =
|
||||
detailProps;
|
||||
|
||||
return <div style={{margin: '8px 16px', display: 'flex', flexDirection:'row',
|
||||
alignItems: 'flex-end', borderRadius: 16,padding: 10, border: '1px solid var(--semi-color-border)'}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/*{uploadNode}*/}
|
||||
{inputNode}
|
||||
{sendNode}
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
margin: '8px 16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
borderRadius: 16,
|
||||
padding: 10,
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/*{uploadNode}*/}
|
||||
{inputNode}
|
||||
{sendNode}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderInputArea = useCallback((props) => {
|
||||
return (<CustomInputRender {...props} />)
|
||||
return <CustomInputRender {...props} />;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout style={{height: '100%'}}>
|
||||
<Layout style={{ height: '100%' }}>
|
||||
{(showSettings || !styleState.isMobile) && (
|
||||
<Layout.Sider style={{ display: styleState.isMobile ? 'block' : 'initial' }}>
|
||||
<Layout.Sider
|
||||
style={{ display: styleState.isMobile ? 'block' : 'initial' }}
|
||||
>
|
||||
<Card style={commonOuterStyle}>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>{t('分组')}:</Typography.Text>
|
||||
@@ -390,18 +434,17 @@ const Playground = () => {
|
||||
setSystemPrompt(value);
|
||||
}}
|
||||
/>
|
||||
|
||||
</Card>
|
||||
</Layout.Sider>
|
||||
)}
|
||||
<Layout.Content>
|
||||
<div style={{height: '100%', position: 'relative'}}>
|
||||
<div style={{ height: '100%', position: 'relative' }}>
|
||||
<SettingsToggle />
|
||||
<Chat
|
||||
chatBoxRenderConfig={{
|
||||
renderChatBoxAction: () => {
|
||||
return <div></div>
|
||||
}
|
||||
return <div></div>;
|
||||
},
|
||||
}}
|
||||
renderInputArea={renderInputArea}
|
||||
roleConfig={roleInfo}
|
||||
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
} from '../../helpers';
|
||||
import { getQuotaPerUnit, renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
||||
import {
|
||||
getQuotaPerUnit,
|
||||
renderQuota,
|
||||
renderQuotaWithPrompt,
|
||||
} from '../../helpers/render';
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
@@ -171,7 +175,9 @@ const EditRedemption = (props) => {
|
||||
/>
|
||||
<Divider />
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Typography.Text>{t('额度') + renderQuotaWithPrompt(quota)}</Typography.Text>
|
||||
<Typography.Text>
|
||||
{t('额度') + renderQuotaWithPrompt(quota)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<AutoComplete
|
||||
style={{ marginTop: 8 }}
|
||||
|
||||
@@ -9,14 +9,14 @@ const Redemption = () => {
|
||||
<>
|
||||
<Layout>
|
||||
<Layout.Header>
|
||||
<h3>{t('管理兑换码')}</h3>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<RedemptionsTable />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
<h3>{t('管理兑换码')}</h3>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<RedemptionsTable />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Redemption;
|
||||
|
||||
@@ -5,23 +5,27 @@ import {
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
showWarning, verifyJSON
|
||||
showWarning,
|
||||
verifyJSON,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
|
||||
const CLAUDE_HEADER = {
|
||||
'claude-3-7-sonnet-20250219-thinking': {
|
||||
'anthropic-beta': ['output-128k-2025-02-19', 'token-efficient-tools-2025-02-19'],
|
||||
}
|
||||
'anthropic-beta': [
|
||||
'output-128k-2025-02-19',
|
||||
'token-efficient-tools-2025-02-19',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const CLAUDE_DEFAULT_MAX_TOKENS = {
|
||||
'default': 8192,
|
||||
"claude-3-haiku-20240307": 4096,
|
||||
"claude-3-opus-20240229": 4096,
|
||||
default: 8192,
|
||||
'claude-3-haiku-20240307': 4096,
|
||||
'claude-3-opus-20240229': 4096,
|
||||
'claude-3-7-sonnet-20250219-thinking': 8192,
|
||||
}
|
||||
};
|
||||
|
||||
export default function SettingClaudeModel(props) {
|
||||
const { t } = useTranslation();
|
||||
@@ -41,7 +45,7 @@ export default function SettingClaudeModel(props) {
|
||||
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,
|
||||
@@ -53,7 +57,8 @@ export default function SettingClaudeModel(props) {
|
||||
if (requestQueue.length === 1) {
|
||||
if (res.includes(undefined)) return;
|
||||
} else if (requestQueue.length > 1) {
|
||||
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
|
||||
if (res.includes(undefined))
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
@@ -92,18 +97,29 @@ export default function SettingClaudeModel(props) {
|
||||
<Form.TextArea
|
||||
label={t('Claude请求头覆盖')}
|
||||
field={'claude.model_headers_settings'}
|
||||
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(CLAUDE_HEADER, null, 2)}
|
||||
extraText={t('示例') + '\n' + JSON.stringify(CLAUDE_HEADER, null, 2)}
|
||||
placeholder={
|
||||
t('为一个 JSON 文本,例如:') +
|
||||
'\n' +
|
||||
JSON.stringify(CLAUDE_HEADER, null, 2)
|
||||
}
|
||||
extraText={
|
||||
t('示例') + '\n' + JSON.stringify(CLAUDE_HEADER, null, 2)
|
||||
}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: t('不是合法的 JSON 字符串')
|
||||
}
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) => setInputs({ ...inputs, 'claude.model_headers_settings': value })}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
'claude.model_headers_settings': value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -112,18 +128,28 @@ export default function SettingClaudeModel(props) {
|
||||
<Form.TextArea
|
||||
label={t('缺省 MaxTokens')}
|
||||
field={'claude.default_max_tokens'}
|
||||
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(CLAUDE_DEFAULT_MAX_TOKENS, null, 2)}
|
||||
extraText={t('示例') + '\n' + JSON.stringify(CLAUDE_DEFAULT_MAX_TOKENS, null, 2)}
|
||||
placeholder={
|
||||
t('为一个 JSON 文本,例如:') +
|
||||
'\n' +
|
||||
JSON.stringify(CLAUDE_DEFAULT_MAX_TOKENS, null, 2)
|
||||
}
|
||||
extraText={
|
||||
t('示例') +
|
||||
'\n' +
|
||||
JSON.stringify(CLAUDE_DEFAULT_MAX_TOKENS, null, 2)
|
||||
}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: t('不是合法的 JSON 字符串')
|
||||
}
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) => setInputs({ ...inputs, 'claude.default_max_tokens': value })}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, 'claude.default_max_tokens': value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -132,7 +158,12 @@ export default function SettingClaudeModel(props) {
|
||||
<Form.Switch
|
||||
label={t('启用Claude思考适配(-thinking后缀)')}
|
||||
field={'claude.thinking_adapter_enabled'}
|
||||
onChange={(value) => setInputs({ ...inputs, 'claude.thinking_adapter_enabled': value })}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
'claude.thinking_adapter_enabled': value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -140,7 +171,9 @@ export default function SettingClaudeModel(props) {
|
||||
<Col span={16}>
|
||||
{/*//展示MaxTokens和BudgetTokens的计算公式, 并展示实际数字*/}
|
||||
<Text>
|
||||
{t('Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比')}
|
||||
{t(
|
||||
'Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比',
|
||||
)}
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -153,7 +186,12 @@ export default function SettingClaudeModel(props) {
|
||||
extraText={t('0.1-1之间的小数')}
|
||||
min={0.1}
|
||||
max={1}
|
||||
onChange={(value) => setInputs({ ...inputs, 'claude.thinking_adapter_budget_tokens_percentage': value })}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
'claude.thinking_adapter_budget_tokens_percentage': value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -5,20 +5,20 @@ import {
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
showWarning, verifyJSON
|
||||
showWarning,
|
||||
verifyJSON,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const GEMINI_SETTING_EXAMPLE = {
|
||||
'default': 'OFF',
|
||||
'HARM_CATEGORY_CIVIC_INTEGRITY': 'BLOCK_NONE',
|
||||
default: 'OFF',
|
||||
HARM_CATEGORY_CIVIC_INTEGRITY: 'BLOCK_NONE',
|
||||
};
|
||||
|
||||
const GEMINI_VERSION_EXAMPLE = {
|
||||
'default': 'v1beta',
|
||||
default: 'v1beta',
|
||||
};
|
||||
|
||||
|
||||
export default function SettingGeminiModel(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -52,7 +52,8 @@ export default function SettingGeminiModel(props) {
|
||||
if (requestQueue.length === 1) {
|
||||
if (res.includes(undefined)) return;
|
||||
} else if (requestQueue.length > 1) {
|
||||
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
|
||||
if (res.includes(undefined))
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
@@ -90,19 +91,27 @@ export default function SettingGeminiModel(props) {
|
||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||
<Form.TextArea
|
||||
label={t('Gemini安全设置')}
|
||||
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(GEMINI_SETTING_EXAMPLE, null, 2)}
|
||||
placeholder={
|
||||
t('为一个 JSON 文本,例如:') +
|
||||
'\n' +
|
||||
JSON.stringify(GEMINI_SETTING_EXAMPLE, null, 2)
|
||||
}
|
||||
field={'gemini.safety_settings'}
|
||||
extraText={t('default为默认设置,可单独设置每个分类的安全等级')}
|
||||
extraText={t(
|
||||
'default为默认设置,可单独设置每个分类的安全等级',
|
||||
)}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: t('不是合法的 JSON 字符串')
|
||||
}
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) => setInputs({ ...inputs, 'gemini.safety_settings': value })}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, 'gemini.safety_settings': value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -110,7 +119,11 @@ export default function SettingGeminiModel(props) {
|
||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||
<Form.TextArea
|
||||
label={t('Gemini版本设置')}
|
||||
placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(GEMINI_VERSION_EXAMPLE, null, 2)}
|
||||
placeholder={
|
||||
t('为一个 JSON 文本,例如:') +
|
||||
'\n' +
|
||||
JSON.stringify(GEMINI_VERSION_EXAMPLE, null, 2)
|
||||
}
|
||||
field={'gemini.version_settings'}
|
||||
extraText={t('default为默认设置,可单独设置每个模型的版本')}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
@@ -119,10 +132,12 @@ export default function SettingGeminiModel(props) {
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: t('不是合法的 JSON 字符串')
|
||||
}
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) => setInputs({ ...inputs, 'gemini.version_settings': value })}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, 'gemini.version_settings': value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
showWarning, verifyJSON
|
||||
showWarning,
|
||||
verifyJSON,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -38,7 +39,8 @@ export default function SettingGlobalModel(props) {
|
||||
if (requestQueue.length === 1) {
|
||||
if (res.includes(undefined)) return;
|
||||
} else if (requestQueue.length > 1) {
|
||||
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
|
||||
if (res.includes(undefined))
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
@@ -77,8 +79,15 @@ export default function SettingGlobalModel(props) {
|
||||
<Form.Switch
|
||||
label={t('启用请求透传')}
|
||||
field={'global.pass_through_request_enabled'}
|
||||
onChange={(value) => setInputs({ ...inputs, 'global.pass_through_request_enabled': value })}
|
||||
extraText={'开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启'}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
'global.pass_through_request_enabled': value,
|
||||
})
|
||||
}
|
||||
extraText={
|
||||
'开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启'
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -15,50 +15,59 @@ export default function GroupRatioSettings(props) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputs, setInputs] = useState({
|
||||
GroupRatio: '',
|
||||
UserUsableGroups: ''
|
||||
UserUsableGroups: '',
|
||||
});
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
|
||||
async function onSubmit() {
|
||||
try {
|
||||
await refForm.current.validate().then(() => {
|
||||
const updateArray = compareObjects(inputs, inputsRow);
|
||||
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
|
||||
|
||||
const requestQueue = updateArray.map((item) => {
|
||||
const value = typeof inputs[item.key] === 'boolean'
|
||||
? String(inputs[item.key])
|
||||
: inputs[item.key];
|
||||
return API.put('/api/option/', { key: item.key, value });
|
||||
});
|
||||
await refForm.current
|
||||
.validate()
|
||||
.then(() => {
|
||||
const updateArray = compareObjects(inputs, inputsRow);
|
||||
if (!updateArray.length)
|
||||
return showWarning(t('你似乎并没有修改什么'));
|
||||
|
||||
setLoading(true);
|
||||
Promise.all(requestQueue)
|
||||
.then((res) => {
|
||||
if (res.includes(undefined)) {
|
||||
return showError(requestQueue.length > 1 ? t('部分保存失败,请重试') : t('保存失败'));
|
||||
}
|
||||
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (!res[i].data.success) {
|
||||
return showError(res[i].data.message);
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Unexpected error:', error);
|
||||
showError(t('保存失败,请重试'));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
const requestQueue = updateArray.map((item) => {
|
||||
const value =
|
||||
typeof inputs[item.key] === 'boolean'
|
||||
? String(inputs[item.key])
|
||||
: inputs[item.key];
|
||||
return API.put('/api/option/', { key: item.key, value });
|
||||
});
|
||||
}).catch(() => {
|
||||
showError(t('请检查输入'));
|
||||
});
|
||||
|
||||
setLoading(true);
|
||||
Promise.all(requestQueue)
|
||||
.then((res) => {
|
||||
if (res.includes(undefined)) {
|
||||
return showError(
|
||||
requestQueue.length > 1
|
||||
? t('部分保存失败,请重试')
|
||||
: t('保存失败'),
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (!res[i].data.success) {
|
||||
return showError(res[i].data.message);
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Unexpected error:', error);
|
||||
showError(t('保存失败,请重试'));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
showError(t('请检查输入'));
|
||||
});
|
||||
} catch (error) {
|
||||
showError(t('请检查输入'));
|
||||
console.error(error);
|
||||
@@ -97,10 +106,12 @@ export default function GroupRatioSettings(props) {
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: t('不是合法的 JSON 字符串')
|
||||
}
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) => setInputs({ ...inputs, GroupRatio: value })}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, GroupRatio: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -116,10 +127,12 @@ export default function GroupRatioSettings(props) {
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: t('不是合法的 JSON 字符串')
|
||||
}
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) => setInputs({ ...inputs, UserUsableGroups: value })}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, UserUsableGroups: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -128,4 +141,4 @@ export default function GroupRatioSettings(props) {
|
||||
<Button onClick={onSubmit}>{t('保存分组倍率设置')}</Button>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Button, Col, Form, Popconfirm, Row, Space, Spin } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Space,
|
||||
Spin,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
compareObjects,
|
||||
API,
|
||||
@@ -24,43 +32,52 @@ export default function ModelRatioSettings(props) {
|
||||
|
||||
async function onSubmit() {
|
||||
try {
|
||||
await refForm.current.validate().then(() => {
|
||||
const updateArray = compareObjects(inputs, inputsRow);
|
||||
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
|
||||
|
||||
const requestQueue = updateArray.map((item) => {
|
||||
const value = typeof inputs[item.key] === 'boolean'
|
||||
? String(inputs[item.key])
|
||||
: inputs[item.key];
|
||||
return API.put('/api/option/', { key: item.key, value });
|
||||
});
|
||||
await refForm.current
|
||||
.validate()
|
||||
.then(() => {
|
||||
const updateArray = compareObjects(inputs, inputsRow);
|
||||
if (!updateArray.length)
|
||||
return showWarning(t('你似乎并没有修改什么'));
|
||||
|
||||
setLoading(true);
|
||||
Promise.all(requestQueue)
|
||||
.then((res) => {
|
||||
if (res.includes(undefined)) {
|
||||
return showError(requestQueue.length > 1 ? t('部分保存失败,请重试') : t('保存失败'));
|
||||
}
|
||||
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (!res[i].data.success) {
|
||||
return showError(res[i].data.message);
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Unexpected error:', error);
|
||||
showError(t('保存失败,请重试'));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
const requestQueue = updateArray.map((item) => {
|
||||
const value =
|
||||
typeof inputs[item.key] === 'boolean'
|
||||
? String(inputs[item.key])
|
||||
: inputs[item.key];
|
||||
return API.put('/api/option/', { key: item.key, value });
|
||||
});
|
||||
}).catch(() => {
|
||||
showError(t('请检查输入'));
|
||||
});
|
||||
|
||||
setLoading(true);
|
||||
Promise.all(requestQueue)
|
||||
.then((res) => {
|
||||
if (res.includes(undefined)) {
|
||||
return showError(
|
||||
requestQueue.length > 1
|
||||
? t('部分保存失败,请重试')
|
||||
: t('保存失败'),
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (!res[i].data.success) {
|
||||
return showError(res[i].data.message);
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Unexpected error:', error);
|
||||
showError(t('保存失败,请重试'));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
showError(t('请检查输入'));
|
||||
});
|
||||
} catch (error) {
|
||||
showError(t('请检查输入'));
|
||||
console.error(error);
|
||||
@@ -106,7 +123,9 @@ export default function ModelRatioSettings(props) {
|
||||
<Form.TextArea
|
||||
label={t('模型固定价格')}
|
||||
extraText={t('一次调用消耗多少刀,优先级大于模型倍率')}
|
||||
placeholder={t('为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀')}
|
||||
placeholder={t(
|
||||
'为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀',
|
||||
)}
|
||||
field={'ModelPrice'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
@@ -114,10 +133,12 @@ export default function ModelRatioSettings(props) {
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: '不是合法的 JSON 字符串'
|
||||
}
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) => setInputs({ ...inputs, ModelPrice: value })}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, ModelPrice: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -133,10 +154,12 @@ export default function ModelRatioSettings(props) {
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: '不是合法的 JSON 字符串'
|
||||
}
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) => setInputs({ ...inputs, ModelRatio: value })}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, ModelRatio: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -152,10 +175,12 @@ export default function ModelRatioSettings(props) {
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: '不是合法的 JSON 字符串'
|
||||
}
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) => setInputs({ ...inputs, CacheRatio: value })}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, CacheRatio: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -172,10 +197,12 @@ export default function ModelRatioSettings(props) {
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: '不是合法的 JSON 字符串'
|
||||
}
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) => setInputs({ ...inputs, CompletionRatio: value })}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, CompletionRatio: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -195,4 +222,4 @@ export default function ModelRatioSettings(props) {
|
||||
</Space>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Table, Button, Input, Modal, Form, Space, Typography, Radio, Notification } from '@douyinfe/semi-ui';
|
||||
import { IconDelete, IconPlus, IconSearch, IconSave, IconBolt } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
Form,
|
||||
Space,
|
||||
Typography,
|
||||
Radio,
|
||||
Notification,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconDelete,
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
IconSave,
|
||||
IconBolt,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { showError, showSuccess } from '../../../helpers';
|
||||
import { API } from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -20,7 +36,8 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
const [batchFillType, setBatchFillType] = useState('ratio');
|
||||
const [batchFillValue, setBatchFillValue] = useState('');
|
||||
const [batchRatioValue, setBatchRatioValue] = useState('');
|
||||
const [batchCompletionRatioValue, setBatchCompletionRatioValue] = useState('');
|
||||
const [batchCompletionRatioValue, setBatchCompletionRatioValue] =
|
||||
useState('');
|
||||
const { Text } = Typography;
|
||||
// 定义可选的每页显示条数
|
||||
const pageSizeOptions = [10, 20, 50, 100];
|
||||
@@ -38,7 +55,7 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
console.error(t('获取启用模型失败:'), error);
|
||||
showError(t('获取启用模型失败'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 获取所有启用的模型
|
||||
@@ -52,20 +69,20 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
const completionRatio = JSON.parse(props.options.CompletionRatio || '{}');
|
||||
|
||||
// 找出所有未设置价格和倍率的模型
|
||||
const unsetModels = enabledModels.filter(modelName => {
|
||||
const unsetModels = enabledModels.filter((modelName) => {
|
||||
const hasPrice = modelPrice[modelName] !== undefined;
|
||||
const hasRatio = modelRatio[modelName] !== undefined;
|
||||
|
||||
|
||||
// 如果模型没有价格或者没有倍率设置,则显示
|
||||
return !hasPrice && !hasRatio;
|
||||
});
|
||||
|
||||
// 创建模型数据
|
||||
const modelData = unsetModels.map(name => ({
|
||||
const modelData = unsetModels.map((name) => ({
|
||||
name,
|
||||
price: modelPrice[name] || '',
|
||||
ratio: modelRatio[name] || '',
|
||||
completionRatio: completionRatio[name] || ''
|
||||
completionRatio: completionRatio[name] || '',
|
||||
}));
|
||||
|
||||
setModels(modelData);
|
||||
@@ -94,8 +111,10 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
};
|
||||
|
||||
// 在 return 语句之前,先处理过滤和分页逻辑
|
||||
const filteredModels = models.filter(model =>
|
||||
searchText ? model.name.toLowerCase().includes(searchText.toLowerCase()) : true
|
||||
const filteredModels = models.filter((model) =>
|
||||
searchText
|
||||
? model.name.toLowerCase().includes(searchText.toLowerCase())
|
||||
: true,
|
||||
);
|
||||
|
||||
// 然后基于过滤后的数据计算分页数据
|
||||
@@ -106,19 +125,23 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
const output = {
|
||||
ModelPrice: JSON.parse(props.options.ModelPrice || '{}'),
|
||||
ModelRatio: JSON.parse(props.options.ModelRatio || '{}'),
|
||||
CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}')
|
||||
CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}'),
|
||||
};
|
||||
|
||||
try {
|
||||
// 数据转换 - 只处理已修改的模型
|
||||
models.forEach(model => {
|
||||
models.forEach((model) => {
|
||||
// 只有当用户设置了值时才更新
|
||||
if (model.price !== '') {
|
||||
// 如果价格不为空,则转换为浮点数,忽略倍率参数
|
||||
output.ModelPrice[model.name] = parseFloat(model.price);
|
||||
} else {
|
||||
if (model.ratio !== '') output.ModelRatio[model.name] = parseFloat(model.ratio);
|
||||
if (model.completionRatio !== '') output.CompletionRatio[model.name] = parseFloat(model.completionRatio);
|
||||
if (model.ratio !== '')
|
||||
output.ModelRatio[model.name] = parseFloat(model.ratio);
|
||||
if (model.completionRatio !== '')
|
||||
output.CompletionRatio[model.name] = parseFloat(
|
||||
model.completionRatio,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -126,13 +149,13 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
const finalOutput = {
|
||||
ModelPrice: JSON.stringify(output.ModelPrice, null, 2),
|
||||
ModelRatio: JSON.stringify(output.ModelRatio, null, 2),
|
||||
CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2)
|
||||
CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2),
|
||||
};
|
||||
|
||||
const requestQueue = Object.entries(finalOutput).map(([key, value]) => {
|
||||
return API.put('/api/option/', {
|
||||
key,
|
||||
value
|
||||
value,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -159,7 +182,6 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
props.refresh();
|
||||
// 重新获取未设置的模型
|
||||
getAllEnabledModels();
|
||||
|
||||
} catch (error) {
|
||||
console.error(t('保存失败:'), error);
|
||||
showError(t('保存失败,请重试'));
|
||||
@@ -182,9 +204,9 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
<Input
|
||||
value={text}
|
||||
placeholder={t('按量计费')}
|
||||
onChange={value => updateModel(record.name, 'price', value)}
|
||||
onChange={(value) => updateModel(record.name, 'price', value)}
|
||||
/>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('模型倍率'),
|
||||
@@ -195,9 +217,9 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
value={text}
|
||||
placeholder={record.price !== '' ? t('模型倍率') : t('输入模型倍率')}
|
||||
disabled={record.price !== ''}
|
||||
onChange={value => updateModel(record.name, 'ratio', value)}
|
||||
onChange={(value) => updateModel(record.name, 'ratio', value)}
|
||||
/>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('补全倍率'),
|
||||
@@ -208,10 +230,12 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
value={text}
|
||||
placeholder={record.price !== '' ? t('补全倍率') : t('输入补全倍率')}
|
||||
disabled={record.price !== ''}
|
||||
onChange={value => updateModel(record.name, 'completionRatio', value)}
|
||||
onChange={(value) =>
|
||||
updateModel(record.name, 'completionRatio', value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const updateModel = (name, field, value) => {
|
||||
@@ -219,27 +243,28 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
showError(t('请输入数字'));
|
||||
return;
|
||||
}
|
||||
setModels(prev =>
|
||||
prev.map(model =>
|
||||
model.name === name
|
||||
? { ...model, [field]: value }
|
||||
: model
|
||||
)
|
||||
setModels((prev) =>
|
||||
prev.map((model) =>
|
||||
model.name === name ? { ...model, [field]: value } : model,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const addModel = (values) => {
|
||||
// 检查模型名称是否存在, 如果存在则拒绝添加
|
||||
if (models.some(model => model.name === values.name)) {
|
||||
if (models.some((model) => model.name === values.name)) {
|
||||
showError(t('模型名称已存在'));
|
||||
return;
|
||||
}
|
||||
setModels(prev => [{
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || ''
|
||||
}, ...prev]);
|
||||
setModels((prev) => [
|
||||
{
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || '',
|
||||
},
|
||||
...prev,
|
||||
]);
|
||||
setVisible(false);
|
||||
showSuccess(t('添加成功'));
|
||||
};
|
||||
@@ -272,39 +297,39 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
}
|
||||
|
||||
// 根据选择的类型批量更新模型
|
||||
setModels(prev =>
|
||||
prev.map(model => {
|
||||
setModels((prev) =>
|
||||
prev.map((model) => {
|
||||
if (selectedRowKeys.includes(model.name)) {
|
||||
if (batchFillType === 'price') {
|
||||
return {
|
||||
...model,
|
||||
price: batchFillValue,
|
||||
ratio: '',
|
||||
completionRatio: ''
|
||||
completionRatio: '',
|
||||
};
|
||||
} else if (batchFillType === 'ratio') {
|
||||
return {
|
||||
...model,
|
||||
price: '',
|
||||
ratio: batchFillValue
|
||||
ratio: batchFillValue,
|
||||
};
|
||||
} else if (batchFillType === 'completionRatio') {
|
||||
return {
|
||||
...model,
|
||||
price: '',
|
||||
completionRatio: batchFillValue
|
||||
completionRatio: batchFillValue,
|
||||
};
|
||||
} else if (batchFillType === 'bothRatio') {
|
||||
return {
|
||||
...model,
|
||||
price: '',
|
||||
ratio: batchRatioValue,
|
||||
completionRatio: batchCompletionRatioValue
|
||||
completionRatio: batchCompletionRatioValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
return model;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
setBatchVisible(false);
|
||||
@@ -312,9 +337,14 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
title: t('批量设置成功'),
|
||||
content: t('已为 {{count}} 个模型设置{{type}}', {
|
||||
count: selectedRowKeys.length,
|
||||
type: batchFillType === 'price' ? t('固定价格') :
|
||||
batchFillType === 'ratio' ? t('模型倍率') :
|
||||
batchFillType === 'completionRatio' ? t('补全倍率') : t('模型倍率和补全倍率')
|
||||
type:
|
||||
batchFillType === 'price'
|
||||
? t('固定价格')
|
||||
: batchFillType === 'ratio'
|
||||
? t('模型倍率')
|
||||
: batchFillType === 'completionRatio'
|
||||
? t('补全倍率')
|
||||
: t('模型倍率和补全倍率'),
|
||||
}),
|
||||
duration: 3,
|
||||
});
|
||||
@@ -323,7 +353,7 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
const handleBatchTypeChange = (value) => {
|
||||
console.log(t('Changing batch type to:'), value);
|
||||
setBatchFillType(value);
|
||||
|
||||
|
||||
// 切换类型时清空对应的值
|
||||
if (value !== 'bothRatio') {
|
||||
setBatchFillValue('');
|
||||
@@ -342,56 +372,63 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space vertical align="start" style={{ width: '100%' }}>
|
||||
<Space vertical align='start' style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Button icon={<IconPlus />} onClick={() => setVisible(true)}>
|
||||
{t('添加模型')}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<IconBolt />}
|
||||
type="secondary"
|
||||
<Button
|
||||
icon={<IconBolt />}
|
||||
type='secondary'
|
||||
onClick={() => setBatchVisible(true)}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
{t('批量设置')} ({selectedRowKeys.length})
|
||||
</Button>
|
||||
<Button type="primary" icon={<IconSave />} onClick={SubmitData} loading={loading}>
|
||||
<Button
|
||||
type='primary'
|
||||
icon={<IconSave />}
|
||||
onClick={SubmitData}
|
||||
loading={loading}
|
||||
>
|
||||
{t('应用更改')}
|
||||
</Button>
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('搜索模型名称')}
|
||||
value={searchText}
|
||||
onChange={value => {
|
||||
setSearchText(value)
|
||||
onChange={(value) => {
|
||||
setSearchText(value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
<Text>{t('此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除')}</Text>
|
||||
|
||||
<Text>
|
||||
{t('此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除')}
|
||||
</Text>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={pagedData}
|
||||
rowSelection={rowSelection}
|
||||
rowKey="name"
|
||||
rowKey='name'
|
||||
pagination={{
|
||||
currentPage: currentPage,
|
||||
pageSize: pageSize,
|
||||
total: filteredModels.length,
|
||||
onPageChange: page => setCurrentPage(page),
|
||||
onPageChange: (page) => setCurrentPage(page),
|
||||
onPageSizeChange: handlePageSizeChange,
|
||||
pageSizeOptions: pageSizeOptions,
|
||||
formatPageText: (page) =>
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
start: page.currentStart,
|
||||
end: page.currentEnd,
|
||||
total: filteredModels.length
|
||||
total: filteredModels.length,
|
||||
}),
|
||||
showTotal: true,
|
||||
showSizeChanger: true
|
||||
showSizeChanger: true,
|
||||
}}
|
||||
empty={
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
@@ -412,45 +449,61 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
>
|
||||
<Form>
|
||||
<Form.Input
|
||||
field="name"
|
||||
field='name'
|
||||
label={t('模型名称')}
|
||||
placeholder="strawberry"
|
||||
placeholder='strawberry'
|
||||
required
|
||||
onChange={value => setCurrentModel(prev => ({ ...prev, name: value }))}
|
||||
onChange={(value) =>
|
||||
setCurrentModel((prev) => ({ ...prev, name: value }))
|
||||
}
|
||||
/>
|
||||
<Form.Switch
|
||||
field="priceMode"
|
||||
label={<>{t('定价模式')}:{currentModel?.priceMode ? t("固定价格") : t("倍率模式")}</>}
|
||||
onChange={checked => {
|
||||
setCurrentModel(prev => ({
|
||||
field='priceMode'
|
||||
label={
|
||||
<>
|
||||
{t('定价模式')}:
|
||||
{currentModel?.priceMode ? t('固定价格') : t('倍率模式')}
|
||||
</>
|
||||
}
|
||||
onChange={(checked) => {
|
||||
setCurrentModel((prev) => ({
|
||||
...prev,
|
||||
price: '',
|
||||
ratio: '',
|
||||
completionRatio: '',
|
||||
priceMode: checked
|
||||
priceMode: checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
{currentModel?.priceMode ? (
|
||||
<Form.Input
|
||||
field="price"
|
||||
field='price'
|
||||
label={t('固定价格(每次)')}
|
||||
placeholder={t('输入每次价格')}
|
||||
onChange={value => setCurrentModel(prev => ({ ...prev, price: value }))}
|
||||
onChange={(value) =>
|
||||
setCurrentModel((prev) => ({ ...prev, price: value }))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Form.Input
|
||||
field="ratio"
|
||||
field='ratio'
|
||||
label={t('模型倍率')}
|
||||
placeholder={t('输入模型倍率')}
|
||||
onChange={value => setCurrentModel(prev => ({ ...prev, ratio: value }))}
|
||||
onChange={(value) =>
|
||||
setCurrentModel((prev) => ({ ...prev, ratio: value }))
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
field="completionRatio"
|
||||
field='completionRatio'
|
||||
label={t('补全倍率')}
|
||||
placeholder={t('输入补全价格')}
|
||||
onChange={value => setCurrentModel(prev => ({ ...prev, completionRatio: value }))}
|
||||
onChange={(value) =>
|
||||
setCurrentModel((prev) => ({
|
||||
...prev,
|
||||
completionRatio: value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -496,50 +549,56 @@ export default function ModelRatioNotSetEditor(props) {
|
||||
</Space>
|
||||
</div>
|
||||
</Form.Section>
|
||||
|
||||
|
||||
{batchFillType === 'bothRatio' ? (
|
||||
<>
|
||||
<Form.Input
|
||||
field="batchRatioValue"
|
||||
field='batchRatioValue'
|
||||
label={t('模型倍率值')}
|
||||
placeholder={t('请输入模型倍率')}
|
||||
value={batchRatioValue}
|
||||
onChange={value => setBatchRatioValue(value)}
|
||||
onChange={(value) => setBatchRatioValue(value)}
|
||||
/>
|
||||
<Form.Input
|
||||
field="batchCompletionRatioValue"
|
||||
field='batchCompletionRatioValue'
|
||||
label={t('补全倍率值')}
|
||||
placeholder={t('请输入补全倍率')}
|
||||
value={batchCompletionRatioValue}
|
||||
onChange={value => setBatchCompletionRatioValue(value)}
|
||||
onChange={(value) => setBatchCompletionRatioValue(value)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Form.Input
|
||||
field="batchFillValue"
|
||||
field='batchFillValue'
|
||||
label={
|
||||
batchFillType === 'price'
|
||||
? t('固定价格值')
|
||||
batchFillType === 'price'
|
||||
? t('固定价格值')
|
||||
: batchFillType === 'ratio'
|
||||
? t('模型倍率值')
|
||||
: t('补全倍率值')
|
||||
}
|
||||
placeholder={t('请输入数值')}
|
||||
value={batchFillValue}
|
||||
onChange={value => setBatchFillValue(value)}
|
||||
onChange={(value) => setBatchFillValue(value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Text type="tertiary">
|
||||
{t('将为选中的 ')} <Text strong>{selectedRowKeys.length}</Text> {t(' 个模型设置相同的值')}
|
||||
|
||||
<Text type='tertiary'>
|
||||
{t('将为选中的 ')} <Text strong>{selectedRowKeys.length}</Text>{' '}
|
||||
{t(' 个模型设置相同的值')}
|
||||
</Text>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="tertiary">
|
||||
{t('当前设置类型: ')} <Text strong>{
|
||||
batchFillType === 'price' ? t('固定价格') :
|
||||
batchFillType === 'ratio' ? t('模型倍率') :
|
||||
batchFillType === 'completionRatio' ? t('补全倍率') : t('模型倍率和补全倍率')
|
||||
}</Text>
|
||||
<Text type='tertiary'>
|
||||
{t('当前设置类型: ')}{' '}
|
||||
<Text strong>
|
||||
{batchFillType === 'price'
|
||||
? t('固定价格')
|
||||
: batchFillType === 'ratio'
|
||||
? t('模型倍率')
|
||||
: batchFillType === 'completionRatio'
|
||||
? t('补全倍率')
|
||||
: t('模型倍率和补全倍率')}
|
||||
</Text>
|
||||
</Text>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
// ModelSettingsVisualEditor.js
|
||||
import React, { useContext, useEffect, useState, useRef } from 'react';
|
||||
import { Table, Button, Input, Modal, Form, Space, RadioGroup, Radio, Tabs, TabPane } from '@douyinfe/semi-ui';
|
||||
import { IconDelete, IconPlus, IconSearch, IconSave, IconEdit } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
Form,
|
||||
Space,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Tabs,
|
||||
TabPane,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconDelete,
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
IconSave,
|
||||
IconEdit,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { showError, showSuccess } from '../../../helpers';
|
||||
import { API } from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -20,7 +37,7 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
const [pricingSubMode, setPricingSubMode] = useState('ratio'); // 'ratio' or 'token-price'
|
||||
const formRef = useRef(null);
|
||||
const pageSize = 10;
|
||||
const quotaPerUnit = getQuotaPerUnit()
|
||||
const quotaPerUnit = getQuotaPerUnit();
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -32,14 +49,15 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
const modelNames = new Set([
|
||||
...Object.keys(modelPrice),
|
||||
...Object.keys(modelRatio),
|
||||
...Object.keys(completionRatio)
|
||||
...Object.keys(completionRatio),
|
||||
]);
|
||||
|
||||
const modelData = Array.from(modelNames).map(name => ({
|
||||
const modelData = Array.from(modelNames).map((name) => ({
|
||||
name,
|
||||
price: modelPrice[name] === undefined ? '' : modelPrice[name],
|
||||
ratio: modelRatio[name] === undefined ? '' : modelRatio[name],
|
||||
completionRatio: completionRatio[name] === undefined ? '' : completionRatio[name]
|
||||
completionRatio:
|
||||
completionRatio[name] === undefined ? '' : completionRatio[name],
|
||||
}));
|
||||
|
||||
setModels(modelData);
|
||||
@@ -56,8 +74,10 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
};
|
||||
|
||||
// 在 return 语句之前,先处理过滤和分页逻辑
|
||||
const filteredModels = models.filter(model =>
|
||||
searchText ? model.name.toLowerCase().includes(searchText.toLowerCase()) : true
|
||||
const filteredModels = models.filter((model) =>
|
||||
searchText
|
||||
? model.name.toLowerCase().includes(searchText.toLowerCase())
|
||||
: true,
|
||||
);
|
||||
|
||||
// 然后基于过滤后的数据计算分页数据
|
||||
@@ -68,20 +88,24 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
const output = {
|
||||
ModelPrice: {},
|
||||
ModelRatio: {},
|
||||
CompletionRatio: {}
|
||||
CompletionRatio: {},
|
||||
};
|
||||
let currentConvertModelName = '';
|
||||
|
||||
try {
|
||||
// 数据转换
|
||||
models.forEach(model => {
|
||||
models.forEach((model) => {
|
||||
currentConvertModelName = model.name;
|
||||
if (model.price !== '') {
|
||||
// 如果价格不为空,则转换为浮点数,忽略倍率参数
|
||||
output.ModelPrice[model.name] = parseFloat(model.price)
|
||||
output.ModelPrice[model.name] = parseFloat(model.price);
|
||||
} else {
|
||||
if (model.ratio !== '') output.ModelRatio[model.name] = parseFloat(model.ratio);
|
||||
if (model.completionRatio !== '') output.CompletionRatio[model.name] = parseFloat(model.completionRatio);
|
||||
if (model.ratio !== '')
|
||||
output.ModelRatio[model.name] = parseFloat(model.ratio);
|
||||
if (model.completionRatio !== '')
|
||||
output.CompletionRatio[model.name] = parseFloat(
|
||||
model.completionRatio,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -89,13 +113,13 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
const finalOutput = {
|
||||
ModelPrice: JSON.stringify(output.ModelPrice, null, 2),
|
||||
ModelRatio: JSON.stringify(output.ModelRatio, null, 2),
|
||||
CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2)
|
||||
CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2),
|
||||
};
|
||||
|
||||
const requestQueue = Object.entries(finalOutput).map(([key, value]) => {
|
||||
return API.put('/api/option/', {
|
||||
key,
|
||||
value
|
||||
value,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,7 +144,6 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
|
||||
showSuccess('保存成功');
|
||||
props.refresh();
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
showError('保存失败,请重试');
|
||||
@@ -143,9 +166,9 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
<Input
|
||||
value={text}
|
||||
placeholder={t('按量计费')}
|
||||
onChange={value => updateModel(record.name, 'price', value)}
|
||||
onChange={(value) => updateModel(record.name, 'price', value)}
|
||||
/>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('模型倍率'),
|
||||
@@ -156,9 +179,9 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
value={text}
|
||||
placeholder={record.price !== '' ? t('模型倍率') : t('默认补全倍率')}
|
||||
disabled={record.price !== ''}
|
||||
onChange={value => updateModel(record.name, 'ratio', value)}
|
||||
onChange={(value) => updateModel(record.name, 'ratio', value)}
|
||||
/>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('补全倍率'),
|
||||
@@ -169,9 +192,11 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
value={text}
|
||||
placeholder={record.price !== '' ? t('补全倍率') : t('默认补全倍率')}
|
||||
disabled={record.price !== ''}
|
||||
onChange={value => updateModel(record.name, 'completionRatio', value)}
|
||||
onChange={(value) =>
|
||||
updateModel(record.name, 'completionRatio', value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('操作'),
|
||||
@@ -179,19 +204,18 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
type='primary'
|
||||
icon={<IconEdit />}
|
||||
onClick={() => editModel(record)}
|
||||
>
|
||||
</Button>
|
||||
></Button>
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type="danger"
|
||||
type='danger'
|
||||
onClick={() => deleteModel(record.name)}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const updateModel = (name, field, value) => {
|
||||
@@ -199,103 +223,114 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
showError('请输入数字');
|
||||
return;
|
||||
}
|
||||
setModels(prev =>
|
||||
prev.map(model =>
|
||||
model.name === name
|
||||
? { ...model, [field]: value }
|
||||
: model
|
||||
)
|
||||
setModels((prev) =>
|
||||
prev.map((model) =>
|
||||
model.name === name ? { ...model, [field]: value } : model,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const deleteModel = (name) => {
|
||||
setModels(prev => prev.filter(model => model.name !== name));
|
||||
setModels((prev) => prev.filter((model) => model.name !== name));
|
||||
};
|
||||
|
||||
|
||||
const calculateRatioFromTokenPrice = (tokenPrice) => {
|
||||
return tokenPrice / 2;
|
||||
};
|
||||
|
||||
const calculateCompletionRatioFromPrices = (modelTokenPrice, completionTokenPrice) => {
|
||||
|
||||
const calculateCompletionRatioFromPrices = (
|
||||
modelTokenPrice,
|
||||
completionTokenPrice,
|
||||
) => {
|
||||
if (!modelTokenPrice || modelTokenPrice === '0') {
|
||||
showError('模型价格不能为0');
|
||||
return '';
|
||||
}
|
||||
return completionTokenPrice / modelTokenPrice;
|
||||
};
|
||||
|
||||
const handleTokenPriceChange = (value) => {
|
||||
|
||||
const handleTokenPriceChange = (value) => {
|
||||
// Use a temporary variable to hold the new state
|
||||
let newState = {
|
||||
...(currentModel || {}),
|
||||
tokenPrice: value,
|
||||
ratio: 0
|
||||
ratio: 0,
|
||||
};
|
||||
|
||||
|
||||
if (!isNaN(value) && value !== '') {
|
||||
const tokenPrice = parseFloat(value);
|
||||
const ratio = calculateRatioFromTokenPrice(tokenPrice);
|
||||
newState.ratio = ratio;
|
||||
}
|
||||
|
||||
|
||||
// Set the state with the complete updated object
|
||||
setCurrentModel(newState);
|
||||
};
|
||||
|
||||
const handleCompletionTokenPriceChange = (value) => {
|
||||
|
||||
const handleCompletionTokenPriceChange = (value) => {
|
||||
// Use a temporary variable to hold the new state
|
||||
let newState = {
|
||||
...(currentModel || {}),
|
||||
completionTokenPrice: value,
|
||||
completionRatio: 0
|
||||
completionRatio: 0,
|
||||
};
|
||||
|
||||
|
||||
if (!isNaN(value) && value !== '' && currentModel?.tokenPrice) {
|
||||
const completionTokenPrice = parseFloat(value);
|
||||
const modelTokenPrice = parseFloat(currentModel.tokenPrice);
|
||||
|
||||
|
||||
if (modelTokenPrice > 0) {
|
||||
const completionRatio = calculateCompletionRatioFromPrices(modelTokenPrice, completionTokenPrice);
|
||||
const completionRatio = calculateCompletionRatioFromPrices(
|
||||
modelTokenPrice,
|
||||
completionTokenPrice,
|
||||
);
|
||||
newState.completionRatio = completionRatio;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set the state with the complete updated object
|
||||
setCurrentModel(newState);
|
||||
};
|
||||
|
||||
const addOrUpdateModel = (values) => {
|
||||
// Check if we're editing an existing model or adding a new one
|
||||
const existingModelIndex = models.findIndex(model => model.name === values.name);
|
||||
|
||||
const existingModelIndex = models.findIndex(
|
||||
(model) => model.name === values.name,
|
||||
);
|
||||
|
||||
if (existingModelIndex >= 0) {
|
||||
// Update existing model
|
||||
setModels(prev => prev.map((model, index) =>
|
||||
index === existingModelIndex ? {
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || ''
|
||||
} : model
|
||||
));
|
||||
setModels((prev) =>
|
||||
prev.map((model, index) =>
|
||||
index === existingModelIndex
|
||||
? {
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || '',
|
||||
}
|
||||
: model,
|
||||
),
|
||||
);
|
||||
setVisible(false);
|
||||
showSuccess(t('更新成功'));
|
||||
} else {
|
||||
// Add new model
|
||||
// Check if model name already exists
|
||||
if (models.some(model => model.name === values.name)) {
|
||||
if (models.some((model) => model.name === values.name)) {
|
||||
showError(t('模型名称已存在'));
|
||||
return;
|
||||
}
|
||||
|
||||
setModels(prev => [{
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || ''
|
||||
}, ...prev]);
|
||||
|
||||
setModels((prev) => [
|
||||
{
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || '',
|
||||
},
|
||||
...prev,
|
||||
]);
|
||||
setVisible(false);
|
||||
showSuccess(t('添加成功'));
|
||||
}
|
||||
@@ -304,7 +339,7 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
const calculateTokenPriceFromRatio = (ratio) => {
|
||||
return ratio * 2;
|
||||
};
|
||||
|
||||
|
||||
const resetModalState = () => {
|
||||
setCurrentModel(null);
|
||||
setPricingMode('per-token');
|
||||
@@ -312,40 +347,43 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
};
|
||||
|
||||
const editModel = (record) => {
|
||||
|
||||
// Determine which pricing mode to use based on the model's current configuration
|
||||
let initialPricingMode = 'per-token';
|
||||
let initialPricingSubMode = 'ratio';
|
||||
|
||||
|
||||
if (record.price !== '') {
|
||||
initialPricingMode = 'per-request';
|
||||
} else {
|
||||
initialPricingMode = 'per-token';
|
||||
// We default to ratio mode, but could set to token-price if needed
|
||||
}
|
||||
|
||||
|
||||
// Set the pricing modes for the form
|
||||
setPricingMode(initialPricingMode);
|
||||
setPricingSubMode(initialPricingSubMode);
|
||||
|
||||
|
||||
// Create a copy of the model data to avoid modifying the original
|
||||
const modelCopy = { ...record };
|
||||
|
||||
|
||||
// If the model has ratio data and we want to populate token price fields
|
||||
if (record.ratio) {
|
||||
modelCopy.tokenPrice = calculateTokenPriceFromRatio(parseFloat(record.ratio)).toString();
|
||||
|
||||
modelCopy.tokenPrice = calculateTokenPriceFromRatio(
|
||||
parseFloat(record.ratio),
|
||||
).toString();
|
||||
|
||||
if (record.completionRatio) {
|
||||
modelCopy.completionTokenPrice = (parseFloat(modelCopy.tokenPrice) * parseFloat(record.completionRatio)).toString();
|
||||
modelCopy.completionTokenPrice = (
|
||||
parseFloat(modelCopy.tokenPrice) * parseFloat(record.completionRatio)
|
||||
).toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set the current model
|
||||
setCurrentModel(modelCopy);
|
||||
|
||||
|
||||
// Open the modal
|
||||
setVisible(true);
|
||||
|
||||
|
||||
// Use setTimeout to ensure the form is rendered before setting values
|
||||
setTimeout(() => {
|
||||
if (formRef.current) {
|
||||
@@ -353,7 +391,7 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
const formValues = {
|
||||
name: modelCopy.name,
|
||||
};
|
||||
|
||||
|
||||
if (initialPricingMode === 'per-request') {
|
||||
formValues.priceInput = modelCopy.price;
|
||||
} else if (initialPricingMode === 'per-token') {
|
||||
@@ -362,7 +400,7 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
formValues.modelTokenPrice = modelCopy.tokenPrice;
|
||||
formValues.completionTokenPrice = modelCopy.completionTokenPrice;
|
||||
}
|
||||
|
||||
|
||||
formRef.current.setValues(formValues);
|
||||
}
|
||||
}, 0);
|
||||
@@ -370,23 +408,26 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space vertical align="start" style={{ width: '100%' }}>
|
||||
<Space vertical align='start' style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Button icon={<IconPlus />} onClick={() => {
|
||||
resetModalState();
|
||||
setVisible(true);
|
||||
}}>
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
onClick={() => {
|
||||
resetModalState();
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
{t('添加模型')}
|
||||
</Button>
|
||||
<Button type="primary" icon={<IconSave />} onClick={SubmitData}>
|
||||
<Button type='primary' icon={<IconSave />} onClick={SubmitData}>
|
||||
{t('应用更改')}
|
||||
</Button>
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('搜索模型名称')}
|
||||
value={searchText}
|
||||
onChange={value => {
|
||||
setSearchText(value)
|
||||
onChange={(value) => {
|
||||
setSearchText(value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
@@ -399,21 +440,27 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
currentPage: currentPage,
|
||||
pageSize: pageSize,
|
||||
total: filteredModels.length,
|
||||
onPageChange: page => setCurrentPage(page),
|
||||
onPageChange: (page) => setCurrentPage(page),
|
||||
formatPageText: (page) =>
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
start: page.currentStart,
|
||||
end: page.currentEnd,
|
||||
total: filteredModels.length
|
||||
total: filteredModels.length,
|
||||
}),
|
||||
showTotal: true,
|
||||
showSizeChanger: false
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
<Modal
|
||||
title={currentModel && currentModel.name && models.some(model => model.name === currentModel.name) ? t('编辑模型') : t('添加模型')}
|
||||
title={
|
||||
currentModel &&
|
||||
currentModel.name &&
|
||||
models.some((model) => model.name === currentModel.name)
|
||||
? t('编辑模型')
|
||||
: t('添加模型')
|
||||
}
|
||||
visible={visible}
|
||||
onCancel={() => {
|
||||
resetModalState();
|
||||
@@ -423,22 +470,33 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
if (currentModel) {
|
||||
// If we're in token price mode, make sure ratio values are properly set
|
||||
const valuesToSave = { ...currentModel };
|
||||
|
||||
if (pricingMode === 'per-token' && pricingSubMode === 'token-price' && currentModel.tokenPrice) {
|
||||
|
||||
if (
|
||||
pricingMode === 'per-token' &&
|
||||
pricingSubMode === 'token-price' &&
|
||||
currentModel.tokenPrice
|
||||
) {
|
||||
// Calculate and set ratio from token price
|
||||
const tokenPrice = parseFloat(currentModel.tokenPrice);
|
||||
valuesToSave.ratio = (tokenPrice / 2).toString();
|
||||
|
||||
|
||||
// Calculate and set completion ratio if both token prices are available
|
||||
if (currentModel.completionTokenPrice && currentModel.tokenPrice) {
|
||||
const completionPrice = parseFloat(currentModel.completionTokenPrice);
|
||||
if (
|
||||
currentModel.completionTokenPrice &&
|
||||
currentModel.tokenPrice
|
||||
) {
|
||||
const completionPrice = parseFloat(
|
||||
currentModel.completionTokenPrice,
|
||||
);
|
||||
const modelPrice = parseFloat(currentModel.tokenPrice);
|
||||
if (modelPrice > 0) {
|
||||
valuesToSave.completionRatio = (completionPrice / modelPrice).toString();
|
||||
valuesToSave.completionRatio = (
|
||||
completionPrice / modelPrice
|
||||
).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Clear price if we're in per-token mode
|
||||
if (pricingMode === 'per-token') {
|
||||
valuesToSave.price = '';
|
||||
@@ -447,139 +505,175 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
valuesToSave.ratio = '';
|
||||
valuesToSave.completionRatio = '';
|
||||
}
|
||||
|
||||
|
||||
addOrUpdateModel(valuesToSave);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form getFormApi={api => formRef.current = api}>
|
||||
<Form getFormApi={(api) => (formRef.current = api)}>
|
||||
<Form.Input
|
||||
field="name"
|
||||
field='name'
|
||||
label={t('模型名称')}
|
||||
placeholder="strawberry"
|
||||
placeholder='strawberry'
|
||||
required
|
||||
disabled={currentModel && currentModel.name && models.some(model => model.name === currentModel.name)}
|
||||
onChange={value => setCurrentModel(prev => ({ ...prev, name: value }))}
|
||||
disabled={
|
||||
currentModel &&
|
||||
currentModel.name &&
|
||||
models.some((model) => model.name === currentModel.name)
|
||||
}
|
||||
onChange={(value) =>
|
||||
setCurrentModel((prev) => ({ ...prev, name: value }))
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
<Form.Section text={t('定价模式')}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<RadioGroup type="button" value={pricingMode} onChange={(e) => {
|
||||
const newMode = e.target.value;
|
||||
const oldMode = pricingMode;
|
||||
setPricingMode(newMode);
|
||||
|
||||
// Instead of resetting all values, convert between modes
|
||||
if (currentModel) {
|
||||
const updatedModel = { ...currentModel };
|
||||
|
||||
// Update formRef with converted values
|
||||
if (formRef.current) {
|
||||
const formValues = {
|
||||
name: updatedModel.name
|
||||
};
|
||||
|
||||
if (newMode === 'per-request') {
|
||||
formValues.priceInput = updatedModel.price || '';
|
||||
} else if (newMode === 'per-token') {
|
||||
formValues.ratioInput = updatedModel.ratio || '';
|
||||
formValues.completionRatioInput = updatedModel.completionRatio || '';
|
||||
formValues.modelTokenPrice = updatedModel.tokenPrice || '';
|
||||
formValues.completionTokenPrice = updatedModel.completionTokenPrice || '';
|
||||
<RadioGroup
|
||||
type='button'
|
||||
value={pricingMode}
|
||||
onChange={(e) => {
|
||||
const newMode = e.target.value;
|
||||
const oldMode = pricingMode;
|
||||
setPricingMode(newMode);
|
||||
|
||||
// Instead of resetting all values, convert between modes
|
||||
if (currentModel) {
|
||||
const updatedModel = { ...currentModel };
|
||||
|
||||
// Update formRef with converted values
|
||||
if (formRef.current) {
|
||||
const formValues = {
|
||||
name: updatedModel.name,
|
||||
};
|
||||
|
||||
if (newMode === 'per-request') {
|
||||
formValues.priceInput = updatedModel.price || '';
|
||||
} else if (newMode === 'per-token') {
|
||||
formValues.ratioInput = updatedModel.ratio || '';
|
||||
formValues.completionRatioInput =
|
||||
updatedModel.completionRatio || '';
|
||||
formValues.modelTokenPrice =
|
||||
updatedModel.tokenPrice || '';
|
||||
formValues.completionTokenPrice =
|
||||
updatedModel.completionTokenPrice || '';
|
||||
}
|
||||
|
||||
formRef.current.setValues(formValues);
|
||||
}
|
||||
|
||||
formRef.current.setValues(formValues);
|
||||
|
||||
// Update the model state
|
||||
setCurrentModel(updatedModel);
|
||||
}
|
||||
|
||||
// Update the model state
|
||||
setCurrentModel(updatedModel);
|
||||
}
|
||||
}}>
|
||||
<Radio value="per-token">{t('按量计费')}</Radio>
|
||||
<Radio value="per-request">{t('按次计费')}</Radio>
|
||||
}}
|
||||
>
|
||||
<Radio value='per-token'>{t('按量计费')}</Radio>
|
||||
<Radio value='per-request'>{t('按次计费')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</Form.Section>
|
||||
|
||||
|
||||
{pricingMode === 'per-token' && (
|
||||
<>
|
||||
<Form.Section text={t('价格设置方式')}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<RadioGroup type="button" value={pricingSubMode} onChange={(e) => {
|
||||
const newSubMode = e.target.value;
|
||||
const oldSubMode = pricingSubMode;
|
||||
setPricingSubMode(newSubMode);
|
||||
|
||||
// Handle conversion between submodes
|
||||
if (currentModel) {
|
||||
const updatedModel = { ...currentModel };
|
||||
|
||||
// Convert between ratio and token price
|
||||
if (oldSubMode === 'ratio' && newSubMode === 'token-price') {
|
||||
if (updatedModel.ratio) {
|
||||
updatedModel.tokenPrice = calculateTokenPriceFromRatio(parseFloat(updatedModel.ratio)).toString();
|
||||
|
||||
if (updatedModel.completionRatio) {
|
||||
updatedModel.completionTokenPrice = (parseFloat(updatedModel.tokenPrice) * parseFloat(updatedModel.completionRatio)).toString();
|
||||
<RadioGroup
|
||||
type='button'
|
||||
value={pricingSubMode}
|
||||
onChange={(e) => {
|
||||
const newSubMode = e.target.value;
|
||||
const oldSubMode = pricingSubMode;
|
||||
setPricingSubMode(newSubMode);
|
||||
|
||||
// Handle conversion between submodes
|
||||
if (currentModel) {
|
||||
const updatedModel = { ...currentModel };
|
||||
|
||||
// Convert between ratio and token price
|
||||
if (
|
||||
oldSubMode === 'ratio' &&
|
||||
newSubMode === 'token-price'
|
||||
) {
|
||||
if (updatedModel.ratio) {
|
||||
updatedModel.tokenPrice =
|
||||
calculateTokenPriceFromRatio(
|
||||
parseFloat(updatedModel.ratio),
|
||||
).toString();
|
||||
|
||||
if (updatedModel.completionRatio) {
|
||||
updatedModel.completionTokenPrice = (
|
||||
parseFloat(updatedModel.tokenPrice) *
|
||||
parseFloat(updatedModel.completionRatio)
|
||||
).toString();
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
oldSubMode === 'token-price' &&
|
||||
newSubMode === 'ratio'
|
||||
) {
|
||||
// Ratio values should already be calculated by the handlers
|
||||
}
|
||||
} else if (oldSubMode === 'token-price' && newSubMode === 'ratio') {
|
||||
// Ratio values should already be calculated by the handlers
|
||||
}
|
||||
|
||||
// Update the form values
|
||||
if (formRef.current) {
|
||||
const formValues = {};
|
||||
|
||||
if (newSubMode === 'ratio') {
|
||||
formValues.ratioInput = updatedModel.ratio || '';
|
||||
formValues.completionRatioInput = updatedModel.completionRatio || '';
|
||||
} else if (newSubMode === 'token-price') {
|
||||
formValues.modelTokenPrice = updatedModel.tokenPrice || '';
|
||||
formValues.completionTokenPrice = updatedModel.completionTokenPrice || '';
|
||||
|
||||
// Update the form values
|
||||
if (formRef.current) {
|
||||
const formValues = {};
|
||||
|
||||
if (newSubMode === 'ratio') {
|
||||
formValues.ratioInput = updatedModel.ratio || '';
|
||||
formValues.completionRatioInput =
|
||||
updatedModel.completionRatio || '';
|
||||
} else if (newSubMode === 'token-price') {
|
||||
formValues.modelTokenPrice =
|
||||
updatedModel.tokenPrice || '';
|
||||
formValues.completionTokenPrice =
|
||||
updatedModel.completionTokenPrice || '';
|
||||
}
|
||||
|
||||
formRef.current.setValues(formValues);
|
||||
}
|
||||
|
||||
formRef.current.setValues(formValues);
|
||||
|
||||
setCurrentModel(updatedModel);
|
||||
}
|
||||
|
||||
setCurrentModel(updatedModel);
|
||||
}
|
||||
}}>
|
||||
<Radio value="ratio">{t('按倍率设置')}</Radio>
|
||||
<Radio value="token-price">{t('按价格设置')}</Radio>
|
||||
}}
|
||||
>
|
||||
<Radio value='ratio'>{t('按倍率设置')}</Radio>
|
||||
<Radio value='token-price'>{t('按价格设置')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</Form.Section>
|
||||
|
||||
|
||||
{pricingSubMode === 'ratio' && (
|
||||
<>
|
||||
<Form.Input
|
||||
field="ratioInput"
|
||||
field='ratioInput'
|
||||
label={t('模型倍率')}
|
||||
placeholder={t('输入模型倍率')}
|
||||
onChange={value => setCurrentModel(prev => ({
|
||||
...prev || {},
|
||||
ratio: value
|
||||
}))}
|
||||
onChange={(value) =>
|
||||
setCurrentModel((prev) => ({
|
||||
...(prev || {}),
|
||||
ratio: value,
|
||||
}))
|
||||
}
|
||||
initValue={currentModel?.ratio || ''}
|
||||
/>
|
||||
<Form.Input
|
||||
field="completionRatioInput"
|
||||
field='completionRatioInput'
|
||||
label={t('补全倍率')}
|
||||
placeholder={t('输入补全倍率')}
|
||||
onChange={value => setCurrentModel(prev => ({
|
||||
...prev || {},
|
||||
completionRatio: value
|
||||
}))}
|
||||
onChange={(value) =>
|
||||
setCurrentModel((prev) => ({
|
||||
...(prev || {}),
|
||||
completionRatio: value,
|
||||
}))
|
||||
}
|
||||
initValue={currentModel?.completionRatio || ''}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{pricingSubMode === 'token-price' && (
|
||||
<>
|
||||
<Form.Input
|
||||
field="modelTokenPrice"
|
||||
field='modelTokenPrice'
|
||||
label={t('输入价格')}
|
||||
onChange={(value) => {
|
||||
handleTokenPriceChange(value);
|
||||
@@ -588,7 +682,7 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
suffix={t('$/1M tokens')}
|
||||
/>
|
||||
<Form.Input
|
||||
field="completionTokenPrice"
|
||||
field='completionTokenPrice'
|
||||
label={t('输出价格')}
|
||||
onChange={(value) => {
|
||||
handleCompletionTokenPriceChange(value);
|
||||
@@ -600,16 +694,18 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{pricingMode === 'per-request' && (
|
||||
<Form.Input
|
||||
field="priceInput"
|
||||
field='priceInput'
|
||||
label={t('固定价格(每次)')}
|
||||
placeholder={t('输入每次价格')}
|
||||
onChange={value => setCurrentModel(prev => ({
|
||||
...prev || {},
|
||||
price: value
|
||||
}))}
|
||||
onChange={(value) =>
|
||||
setCurrentModel((prev) => ({
|
||||
...(prev || {}),
|
||||
price: value,
|
||||
}))
|
||||
}
|
||||
initValue={currentModel?.price || ''}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Banner, Button, Col, Form, Popconfirm, Row, Space, Spin } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Space,
|
||||
Spin,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
compareObjects,
|
||||
API,
|
||||
@@ -7,7 +16,7 @@ import {
|
||||
showSuccess,
|
||||
showWarning,
|
||||
verifyJSON,
|
||||
verifyJSONPromise
|
||||
verifyJSONPromise,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -15,7 +24,7 @@ export default function SettingsChats(props) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputs, setInputs] = useState({
|
||||
Chats: "[]",
|
||||
Chats: '[]',
|
||||
});
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
@@ -23,44 +32,48 @@ export default function SettingsChats(props) {
|
||||
async function onSubmit() {
|
||||
try {
|
||||
console.log('Starting validation...');
|
||||
await refForm.current.validate().then(() => {
|
||||
console.log('Validation passed');
|
||||
const updateArray = compareObjects(inputs, inputsRow);
|
||||
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
|
||||
const requestQueue = updateArray.map((item) => {
|
||||
let value = '';
|
||||
if (typeof inputs[item.key] === 'boolean') {
|
||||
value = String(inputs[item.key]);
|
||||
} else {
|
||||
value = 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('部分保存失败,请重试'));
|
||||
await refForm.current
|
||||
.validate()
|
||||
.then(() => {
|
||||
console.log('Validation passed');
|
||||
const updateArray = compareObjects(inputs, inputsRow);
|
||||
if (!updateArray.length)
|
||||
return showWarning(t('你似乎并没有修改什么'));
|
||||
const requestQueue = updateArray.map((item) => {
|
||||
let value = '';
|
||||
if (typeof inputs[item.key] === 'boolean') {
|
||||
value = String(inputs[item.key]);
|
||||
} else {
|
||||
value = inputs[item.key];
|
||||
}
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
})
|
||||
.catch(() => {
|
||||
showError(t('保存失败,请重试'));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
return API.put('/api/option/', {
|
||||
key: item.key,
|
||||
value,
|
||||
});
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error('Validation failed:', error);
|
||||
showError(t('请检查输入'));
|
||||
});
|
||||
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('保存成功'));
|
||||
props.refresh();
|
||||
})
|
||||
.catch(() => {
|
||||
showError(t('保存失败,请重试'));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Validation failed:', error);
|
||||
showError(t('请检查输入'));
|
||||
});
|
||||
} catch (error) {
|
||||
showError(t('请检查输入'));
|
||||
console.error(error);
|
||||
@@ -109,11 +122,15 @@ export default function SettingsChats(props) {
|
||||
<Form.Section text={t('令牌聊天设置')}>
|
||||
<Banner
|
||||
type='warning'
|
||||
description={t('必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能')}
|
||||
description={t(
|
||||
'必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能',
|
||||
)}
|
||||
/>
|
||||
<Banner
|
||||
type='info'
|
||||
description={t('链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1')}
|
||||
description={t(
|
||||
'链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1',
|
||||
)}
|
||||
/>
|
||||
<Form.TextArea
|
||||
label={t('聊天配置')}
|
||||
@@ -128,22 +145,20 @@ export default function SettingsChats(props) {
|
||||
validator: (rule, value) => {
|
||||
return verifyJSON(value);
|
||||
},
|
||||
message: t('不是合法的 JSON 字符串')
|
||||
}
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
Chats: value
|
||||
Chats: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
<Space>
|
||||
<Button onClick={onSubmit}>
|
||||
{t('保存聊天设置')}
|
||||
</Button>
|
||||
<Button onClick={onSubmit}>{t('保存聊天设置')}</Button>
|
||||
</Space>
|
||||
</Spin>
|
||||
);
|
||||
|
||||
@@ -42,7 +42,8 @@ export default function SettingsCreditLimit(props) {
|
||||
if (requestQueue.length === 1) {
|
||||
if (res.includes(undefined)) return;
|
||||
} else if (requestQueue.length > 1) {
|
||||
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
|
||||
if (res.includes(undefined))
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function DataDashboard(props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const optionsDataExportDefaultTime = [
|
||||
{ key: 'hour', label: t('小时'), value: 'hour' },
|
||||
{ key: 'day', label: t('天'), value: 'day' },
|
||||
@@ -47,7 +47,8 @@ export default function DataDashboard(props) {
|
||||
if (requestQueue.length === 1) {
|
||||
if (res.includes(undefined)) return;
|
||||
} else if (requestQueue.length > 1) {
|
||||
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
|
||||
if (res.includes(undefined))
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
|
||||
@@ -44,7 +44,8 @@ export default function SettingsDrawing(props) {
|
||||
if (requestQueue.length === 1) {
|
||||
if (res.includes(undefined)) return;
|
||||
} else if (requestQueue.length > 1) {
|
||||
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
|
||||
if (res.includes(undefined))
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
@@ -146,7 +147,8 @@ export default function SettingsDrawing(props) {
|
||||
label={
|
||||
<>
|
||||
{t('开启之后会清除用户提示词中的')} <Tag>--fast</Tag> 、
|
||||
<Tag>--relax</Tag> {t('以及')} <Tag>--turbo</Tag> {t('参数')}
|
||||
<Tag>--relax</Tag> {t('以及')} <Tag>--turbo</Tag>{' '}
|
||||
{t('参数')}
|
||||
</>
|
||||
}
|
||||
size='default'
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Banner, Button, Col, Form, Row, Spin, Collapse, Modal } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
Row,
|
||||
Spin,
|
||||
Collapse,
|
||||
Modal,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
compareObjects,
|
||||
API,
|
||||
@@ -54,7 +63,8 @@ export default function GeneralSettings(props) {
|
||||
if (requestQueue.length === 1) {
|
||||
if (res.includes(undefined)) return;
|
||||
} else if (requestQueue.length > 1) {
|
||||
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
|
||||
if (res.includes(undefined))
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
@@ -198,7 +208,7 @@ export default function GeneralSettings(props) {
|
||||
</Form.Section>
|
||||
</Form>
|
||||
</Spin>
|
||||
|
||||
|
||||
<Modal
|
||||
title={t('警告')}
|
||||
visible={showQuotaWarning}
|
||||
@@ -209,7 +219,9 @@ export default function GeneralSettings(props) {
|
||||
>
|
||||
<Banner
|
||||
type='warning'
|
||||
description={t('此设置用于系统内部计算,默认值500000是为了精确到6位小数点设计,不推荐修改。')}
|
||||
description={t(
|
||||
'此设置用于系统内部计算,默认值500000是为了精确到6位小数点设计,不推荐修改。',
|
||||
)}
|
||||
bordered
|
||||
fullMode={false}
|
||||
closeIcon={null}
|
||||
|
||||
@@ -45,7 +45,8 @@ export default function SettingsLog(props) {
|
||||
if (requestQueue.length === 1) {
|
||||
if (res.includes(undefined)) return;
|
||||
} else if (requestQueue.length > 1) {
|
||||
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
|
||||
if (res.includes(undefined))
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
showWarning, verifyJSON
|
||||
showWarning,
|
||||
verifyJSON,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -43,7 +44,8 @@ export default function SettingsMonitoring(props) {
|
||||
if (requestQueue.length === 1) {
|
||||
if (res.includes(undefined)) return;
|
||||
} else if (requestQueue.length > 1) {
|
||||
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
|
||||
if (res.includes(undefined))
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
@@ -67,7 +69,7 @@ export default function SettingsMonitoring(props) {
|
||||
setInputsRow(structuredClone(currentInputs));
|
||||
refForm.current.setValues(currentInputs);
|
||||
}, [props.options]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={loading}>
|
||||
@@ -84,7 +86,9 @@ export default function SettingsMonitoring(props) {
|
||||
step={1}
|
||||
min={0}
|
||||
suffix={t('秒')}
|
||||
extraText={t('当运行通道全部测试时,超过此时间将自动禁用通道')}
|
||||
extraText={t(
|
||||
'当运行通道全部测试时,超过此时间将自动禁用通道',
|
||||
)}
|
||||
placeholder={''}
|
||||
field={'ChannelDisableThreshold'}
|
||||
onChange={(value) =>
|
||||
@@ -150,10 +154,14 @@ export default function SettingsMonitoring(props) {
|
||||
<Form.TextArea
|
||||
label={t('自动禁用关键词')}
|
||||
placeholder={t('一行一个,不区分大小写')}
|
||||
extraText={t('当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道')}
|
||||
extraText={t(
|
||||
'当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道',
|
||||
)}
|
||||
field={'AutomaticDisableKeywords'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
onChange={(value) => setInputs({ ...inputs, AutomaticDisableKeywords: value })}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, AutomaticDisableKeywords: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -41,7 +41,8 @@ export default function SettingsSensitiveWords(props) {
|
||||
if (requestQueue.length === 1) {
|
||||
if (res.includes(undefined)) return;
|
||||
} else if (requestQueue.length > 1) {
|
||||
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
|
||||
if (res.includes(undefined))
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function RequestRateLimit(props) {
|
||||
ModelRequestRateLimitEnabled: false,
|
||||
ModelRequestRateLimitCount: -1,
|
||||
ModelRequestRateLimitSuccessCount: 1000,
|
||||
ModelRequestRateLimitDurationMinutes: 1
|
||||
ModelRequestRateLimitDurationMinutes: 1,
|
||||
});
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
@@ -43,7 +43,8 @@ export default function RequestRateLimit(props) {
|
||||
if (requestQueue.length === 1) {
|
||||
if (res.includes(undefined)) return;
|
||||
} else if (requestQueue.length > 1) {
|
||||
if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
|
||||
if (res.includes(undefined))
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
import React, { useContext, useEffect, useState, useRef } from 'react';
|
||||
import { Card, Col, Row, Form, Button, Typography, Space, RadioGroup, Radio, Modal, Banner } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Card,
|
||||
Col,
|
||||
Row,
|
||||
Form,
|
||||
Button,
|
||||
Typography,
|
||||
Space,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Modal,
|
||||
Banner,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { API, showError, showNotice, timestamp2string } from '../../helpers';
|
||||
import { StatusContext } from '../../context/Status';
|
||||
import { marked } from 'marked';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconHelpCircle, IconInfoCircle, IconAlertTriangle } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
IconHelpCircle,
|
||||
IconInfoCircle,
|
||||
IconAlertTriangle,
|
||||
} from '@douyinfe/semi-icons';
|
||||
|
||||
const Setup = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
@@ -16,16 +32,16 @@ const Setup = () => {
|
||||
const [setupStatus, setSetupStatus] = useState({
|
||||
status: false,
|
||||
root_init: false,
|
||||
database_type: ''
|
||||
database_type: '',
|
||||
});
|
||||
const { Text, Title } = Typography;
|
||||
const formRef = useRef(null);
|
||||
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
usageMode: 'external'
|
||||
usageMode: 'external',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -38,7 +54,7 @@ const Setup = () => {
|
||||
const { success, data } = res.data;
|
||||
if (success) {
|
||||
setSetupStatus(data);
|
||||
|
||||
|
||||
// If setup is already completed, redirect to home
|
||||
if (data.status) {
|
||||
window.location.href = '/';
|
||||
@@ -53,54 +69,54 @@ const Setup = () => {
|
||||
};
|
||||
|
||||
const handleUsageModeChange = (val) => {
|
||||
setFormData({...formData, usageMode: val});
|
||||
setFormData({ ...formData, usageMode: val });
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
if (!formRef.current) {
|
||||
console.error("Form reference is null");
|
||||
console.error('Form reference is null');
|
||||
showError(t('表单引用错误,请刷新页面重试'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const values = formRef.current.getValues();
|
||||
console.log("Form values:", values);
|
||||
|
||||
console.log('Form values:', values);
|
||||
|
||||
// For root_init=false, validate admin username and password
|
||||
if (!setupStatus.root_init) {
|
||||
if (!values.username || !values.username.trim()) {
|
||||
showError(t('请输入管理员用户名'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!values.password || values.password.length < 8) {
|
||||
showError(t('密码长度至少为8个字符'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (values.password !== values.confirmPassword) {
|
||||
showError(t('两次输入的密码不一致'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Prepare submission data
|
||||
const formValues = {...values};
|
||||
const formValues = { ...values };
|
||||
formValues.SelfUseModeEnabled = values.usageMode === 'self';
|
||||
formValues.DemoSiteEnabled = values.usageMode === 'demo';
|
||||
|
||||
|
||||
// Remove usageMode as it's not needed by the backend
|
||||
delete formValues.usageMode;
|
||||
|
||||
console.log("Submitting data to backend:", formValues);
|
||||
|
||||
console.log('Submitting data to backend:', formValues);
|
||||
setLoading(true);
|
||||
|
||||
|
||||
// Submit to backend
|
||||
API.post('/api/setup', formValues)
|
||||
.then(res => {
|
||||
.then((res) => {
|
||||
const { success, message } = res.data;
|
||||
console.log("API response:", res.data);
|
||||
|
||||
console.log('API response:', res.data);
|
||||
|
||||
if (success) {
|
||||
showNotice(t('系统初始化成功,正在跳转...'));
|
||||
setTimeout(() => {
|
||||
@@ -110,7 +126,7 @@ const Setup = () => {
|
||||
showError(message || t('初始化失败,请重试'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('API error:', error);
|
||||
showError(t('系统初始化失败,请重试'));
|
||||
setLoading(false);
|
||||
@@ -124,31 +140,44 @@ const Setup = () => {
|
||||
<>
|
||||
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
|
||||
<Card>
|
||||
<Title heading={2} style={{ marginBottom: '24px' }}>{t('系统初始化')}</Title>
|
||||
|
||||
<Title heading={2} style={{ marginBottom: '24px' }}>
|
||||
{t('系统初始化')}
|
||||
</Title>
|
||||
|
||||
{setupStatus.database_type === 'sqlite' && (
|
||||
<Banner
|
||||
type="warning"
|
||||
icon={<IconAlertTriangle size="large" />}
|
||||
type='warning'
|
||||
icon={<IconAlertTriangle size='large' />}
|
||||
closeIcon={null}
|
||||
title={t('数据库警告')}
|
||||
description={
|
||||
<div>
|
||||
<p>{t('您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!')}</p>
|
||||
<p>{t('建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。')}</p>
|
||||
<p>
|
||||
{t(
|
||||
'您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
'建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
<Form
|
||||
getFormApi={(formApi) => { formRef.current = formApi; console.log("Form API set:", formApi); }}
|
||||
getFormApi={(formApi) => {
|
||||
formRef.current = formApi;
|
||||
console.log('Form API set:', formApi);
|
||||
}}
|
||||
initValues={formData}
|
||||
>
|
||||
{setupStatus.root_init ? (
|
||||
<Banner
|
||||
type="info"
|
||||
type='info'
|
||||
icon={<IconInfoCircle />}
|
||||
closeIcon={null}
|
||||
description={t('管理员账号已经初始化过,请继续设置系统参数')}
|
||||
@@ -157,43 +186,56 @@ const Setup = () => {
|
||||
) : (
|
||||
<Form.Section text={t('管理员账号')}>
|
||||
<Form.Input
|
||||
field="username"
|
||||
field='username'
|
||||
label={t('用户名')}
|
||||
placeholder={t('请输入管理员用户名')}
|
||||
showClear
|
||||
onChange={(value) => setFormData({...formData, username: value})}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, username: value })
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
field="password"
|
||||
field='password'
|
||||
label={t('密码')}
|
||||
placeholder={t('请输入管理员密码')}
|
||||
type="password"
|
||||
type='password'
|
||||
showClear
|
||||
onChange={(value) => setFormData({...formData, password: value})}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, password: value })
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
field="confirmPassword"
|
||||
field='confirmPassword'
|
||||
label={t('确认密码')}
|
||||
placeholder={t('请确认管理员密码')}
|
||||
type="password"
|
||||
type='password'
|
||||
showClear
|
||||
onChange={(value) => setFormData({...formData, confirmPassword: value})}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, confirmPassword: value })
|
||||
}
|
||||
/>
|
||||
</Form.Section>
|
||||
)}
|
||||
|
||||
<Form.Section text={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{t('系统设置')}
|
||||
</div>
|
||||
}>
|
||||
<Form.RadioGroup
|
||||
field="usageMode"
|
||||
|
||||
<Form.Section
|
||||
text={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{t('系统设置')}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form.RadioGroup
|
||||
field='usageMode'
|
||||
label={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{t('使用模式')}
|
||||
<IconHelpCircle
|
||||
style={{ marginLeft: '4px', color: 'var(--semi-color-primary)', verticalAlign: 'middle', cursor: 'pointer' }}
|
||||
<IconHelpCircle
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
color: 'var(--semi-color-primary)',
|
||||
verticalAlign: 'middle',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
@@ -203,18 +245,18 @@ const Setup = () => {
|
||||
</div>
|
||||
}
|
||||
extraText={t('可在初始化后修改')}
|
||||
initValue="external"
|
||||
initValue='external'
|
||||
onChange={handleUsageModeChange}
|
||||
>
|
||||
<Form.Radio value="external">{t('对外运营模式')}</Form.Radio>
|
||||
<Form.Radio value="self">{t('自用模式')}</Form.Radio>
|
||||
<Form.Radio value="demo">{t('演示站点模式')}</Form.Radio>
|
||||
<Form.Radio value='external'>{t('对外运营模式')}</Form.Radio>
|
||||
<Form.Radio value='self'>{t('自用模式')}</Form.Radio>
|
||||
<Form.Radio value='demo'>{t('演示站点模式')}</Form.Radio>
|
||||
</Form.RadioGroup>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
|
||||
<div style={{ marginTop: '24px', textAlign: 'right' }}>
|
||||
<Button type="primary" onClick={onSubmit} loading={loading}>
|
||||
<Button type='primary' onClick={onSubmit} loading={loading}>
|
||||
{t('初始化系统')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -233,12 +275,18 @@ const Setup = () => {
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<Title heading={6}>{t('对外运营模式')}</Title>
|
||||
<p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p>
|
||||
<p>{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}</p>
|
||||
<p>
|
||||
{t(
|
||||
'此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<Title heading={6}>{t('自用模式')}</Title>
|
||||
<p>{t('适用于个人使用的场景。')}</p>
|
||||
<p>{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}</p>
|
||||
<p>
|
||||
{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<Title heading={6}>{t('演示站点模式')}</Title>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import TaskLogsTable from "../../components/TaskLogsTable.js";
|
||||
import TaskLogsTable from '../../components/TaskLogsTable.js';
|
||||
|
||||
const Task = () => (
|
||||
<>
|
||||
|
||||
@@ -18,8 +18,9 @@ import {
|
||||
Select,
|
||||
SideSheet,
|
||||
Space,
|
||||
Spin, TextArea,
|
||||
Typography
|
||||
Spin,
|
||||
TextArea,
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||
import { Divider } from 'semantic-ui-react';
|
||||
@@ -47,7 +48,7 @@ const EditToken = (props) => {
|
||||
model_limits_enabled,
|
||||
model_limits,
|
||||
allow_ips,
|
||||
group
|
||||
group,
|
||||
} = inputs;
|
||||
// const [visible, setVisible] = useState(false);
|
||||
const [models, setModels] = useState([]);
|
||||
@@ -100,7 +101,7 @@ const EditToken = (props) => {
|
||||
let localGroupOptions = Object.entries(data).map(([group, info]) => ({
|
||||
label: info.desc,
|
||||
value: group,
|
||||
ratio: info.ratio
|
||||
ratio: info.ratio,
|
||||
}));
|
||||
setGroups(localGroupOptions);
|
||||
} else {
|
||||
@@ -229,9 +230,7 @@ const EditToken = (props) => {
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
showSuccess(
|
||||
t('令牌创建成功,请在列表页面点击复制获取令牌!')
|
||||
);
|
||||
showSuccess(t('令牌创建成功,请在列表页面点击复制获取令牌!'));
|
||||
props.refresh();
|
||||
props.handleClose();
|
||||
}
|
||||
@@ -246,7 +245,9 @@ const EditToken = (props) => {
|
||||
<SideSheet
|
||||
placement={isEdit ? 'right' : 'left'}
|
||||
title={
|
||||
<Title level={3}>{isEdit ? t('更新令牌信息') : t('创建新的令牌')}</Title>
|
||||
<Title level={3}>
|
||||
{isEdit ? t('更新令牌信息') : t('创建新的令牌')}
|
||||
</Title>
|
||||
}
|
||||
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
||||
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
||||
@@ -333,7 +334,9 @@ const EditToken = (props) => {
|
||||
<Divider />
|
||||
<Banner
|
||||
type={'warning'}
|
||||
description={t('注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。')}
|
||||
description={t(
|
||||
'注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。',
|
||||
)}
|
||||
></Banner>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Typography.Text>{`${t('额度')}${renderQuotaWithPrompt(remain_quota)}`}</Typography.Text>
|
||||
@@ -396,7 +399,9 @@ const EditToken = (props) => {
|
||||
</div>
|
||||
<Divider />
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text>{t('IP白名单(请勿过度信任此功能)')}</Typography.Text>
|
||||
<Typography.Text>
|
||||
{t('IP白名单(请勿过度信任此功能)')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
label={t('IP白名单')}
|
||||
@@ -440,7 +445,7 @@ const EditToken = (props) => {
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text>{t('令牌分组,默认为用户的分组')}</Typography.Text>
|
||||
</div>
|
||||
{groups.length > 0 ?
|
||||
{groups.length > 0 ? (
|
||||
<Select
|
||||
style={{ marginTop: 8 }}
|
||||
placeholder={t('令牌分组,默认为用户的分组')}
|
||||
@@ -455,14 +460,15 @@ const EditToken = (props) => {
|
||||
value={inputs.group}
|
||||
autoComplete='new-password'
|
||||
optionList={groups}
|
||||
/>:
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
style={{ marginTop: 8 }}
|
||||
placeholder={t('管理员未设置用户可选分组')}
|
||||
name='gruop'
|
||||
disabled={true}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</Spin>
|
||||
</SideSheet>
|
||||
</>
|
||||
|
||||
@@ -8,13 +8,15 @@ const Token = () => {
|
||||
<>
|
||||
<Layout>
|
||||
<Layout.Header>
|
||||
<Banner
|
||||
type='warning'
|
||||
description={t('令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。')}
|
||||
/>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<TokensTable />
|
||||
<Banner
|
||||
type='warning'
|
||||
description={t(
|
||||
'令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。',
|
||||
)}
|
||||
/>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<TokensTable />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</>
|
||||
|
||||
@@ -228,8 +228,12 @@ const TopUp = () => {
|
||||
size={'small'}
|
||||
centered={true}
|
||||
>
|
||||
<p>{t('充值数量')}:{topUpCount}</p>
|
||||
<p>{t('实付金额')}:{renderAmount()}</p>
|
||||
<p>
|
||||
{t('充值数量')}:{topUpCount}
|
||||
</p>
|
||||
<p>
|
||||
{t('实付金额')}:{renderAmount()}
|
||||
</p>
|
||||
<p>{t('是否确认充值?')}</p>
|
||||
</Modal>
|
||||
<div
|
||||
@@ -280,7 +284,9 @@ const TopUp = () => {
|
||||
disabled={!enableOnlineTopUp}
|
||||
field={'redemptionCount'}
|
||||
label={t('实付金额:') + ' ' + renderAmount()}
|
||||
placeholder={t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)}
|
||||
placeholder={
|
||||
t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
|
||||
}
|
||||
name='redemptionCount'
|
||||
type={'number'}
|
||||
value={topUpCount}
|
||||
|
||||
@@ -201,7 +201,9 @@ const EditUser = (props) => {
|
||||
search
|
||||
selection
|
||||
allowAdditions
|
||||
additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
|
||||
additionLabel={t(
|
||||
'请在系统设置页面编辑分组倍率以添加新的分组:',
|
||||
)}
|
||||
onChange={(value) => handleInputChange('group', value)}
|
||||
value={inputs.group}
|
||||
autoComplete='new-password'
|
||||
@@ -231,17 +233,21 @@ const EditUser = (props) => {
|
||||
name='github_id'
|
||||
value={github_id}
|
||||
autoComplete='new-password'
|
||||
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
|
||||
placeholder={t(
|
||||
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
||||
)}
|
||||
readonly
|
||||
/>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Typography.Text>{t('`已绑定的 OIDC 账户')}</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='oidc_id'
|
||||
value={oidc_id}
|
||||
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
|
||||
readonly
|
||||
name='oidc_id'
|
||||
value={oidc_id}
|
||||
placeholder={t(
|
||||
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
||||
)}
|
||||
readonly
|
||||
/>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Typography.Text>{t('已绑定的微信账户')}</Typography.Text>
|
||||
@@ -250,7 +256,9 @@ const EditUser = (props) => {
|
||||
name='wechat_id'
|
||||
value={wechat_id}
|
||||
autoComplete='new-password'
|
||||
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
|
||||
placeholder={t(
|
||||
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
||||
)}
|
||||
readonly
|
||||
/>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
@@ -260,7 +268,9 @@ const EditUser = (props) => {
|
||||
name='email'
|
||||
value={email}
|
||||
autoComplete='new-password'
|
||||
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
|
||||
placeholder={t(
|
||||
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
||||
)}
|
||||
readonly
|
||||
/>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
@@ -270,7 +280,9 @@ const EditUser = (props) => {
|
||||
name='telegram_id'
|
||||
value={telegram_id}
|
||||
autoComplete='new-password'
|
||||
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
|
||||
placeholder={t(
|
||||
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
|
||||
)}
|
||||
readonly
|
||||
/>
|
||||
</Spin>
|
||||
|
||||
@@ -9,10 +9,10 @@ const User = () => {
|
||||
<>
|
||||
<Layout>
|
||||
<Layout.Header>
|
||||
<h3>{t('管理用户')}</h3>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<UsersTable />
|
||||
<h3>{t('管理用户')}</h3>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<UsersTable />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user