Merge branch 'main' into ui

This commit is contained in:
Apple\Apple
2025-05-17 11:22:22 +08:00
53 changed files with 1596 additions and 325 deletions

View File

@@ -618,7 +618,6 @@ const LogsTable = () => {
</Paragraph>
);
}
let content = other?.claude
? renderClaudeModelPriceSimple(
other.model_ratio,
@@ -935,6 +934,13 @@ const LogsTable = () => {
other.model_price,
other.group_ratio,
other?.user_group_ratio,
false,
1.0,
undefined,
other.web_search || false,
other.web_search_call_count || 0,
other.file_search || false,
other.file_search_call_count || 0,
),
});
}
@@ -995,6 +1001,12 @@ const LogsTable = () => {
other?.image || false,
other?.image_ratio || 0,
other?.image_output || 0,
other?.web_search || false,
other?.web_search_call_count || 0,
other?.web_search_price || 0,
other?.file_search || false,
other?.file_search_call_count || 0,
other?.file_search_price || 0,
);
}
expandDataLocal.push({

View File

@@ -57,6 +57,7 @@ const PersonalSetting = () => {
email_verification_code: '',
email: '',
self_account_deletion_confirmation: '',
original_password: '',
set_new_password: '',
set_new_password_confirmation: '',
});
@@ -239,11 +240,24 @@ const PersonalSetting = () => {
};
const changePassword = async () => {
if (inputs.original_password === '') {
showError(t('请输入原密码!'));
return;
}
if (inputs.set_new_password === '') {
showError(t('请输入新密码!'));
return;
}
if (inputs.original_password === inputs.set_new_password) {
showError(t('新密码需要和原密码不一致!'));
return;
}
if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
showError(t('两次输入的密码不一致!'));
return;
}
const res = await API.put(`/api/user/self`, {
original_password: inputs.original_password,
password: inputs.set_new_password,
});
const { success, message } = res.data;
@@ -816,8 +830,8 @@ const PersonalSetting = () => {
</div>
</Card>
<Card style={{ marginTop: 10 }}>
<Tabs type="line" defaultActiveKey="notification">
<TabPane tab={t('通知设置')} itemKey="notification">
<Tabs type='line' defaultActiveKey='notification'>
<TabPane tab={t('通知设置')} itemKey='notification'>
<div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('通知方式')}</Typography.Text>
<div style={{ marginTop: 10 }}>
@@ -993,23 +1007,36 @@ const PersonalSetting = () => {
</Typography.Text>
</div>
</TabPane>
<TabPane tab={t('价格设置')} itemKey="price">
<TabPane tab={t('价格设置')} itemKey='price'>
<div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('接受未设置价格模型')}</Typography.Text>
<Typography.Text strong>
{t('接受未设置价格模型')}
</Typography.Text>
<div style={{ marginTop: 10 }}>
<Checkbox
checked={notificationSettings.acceptUnsetModelRatioModel}
onChange={e => handleNotificationSettingChange('acceptUnsetModelRatioModel', e.target.checked)}
checked={
notificationSettings.acceptUnsetModelRatioModel
}
onChange={(e) =>
handleNotificationSettingChange(
'acceptUnsetModelRatioModel',
e.target.checked,
)
}
>
{t('接受未设置价格模型')}
</Checkbox>
<Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
{t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
<Typography.Text
type='secondary'
style={{ marginTop: 8, display: 'block' }}
>
{t(
'当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用',
)}
</Typography.Text>
</div>
</div>
</TabPane>
</Tabs>
<div style={{ marginTop: 20 }}>
<Button type='primary' onClick={saveNotificationSettings}>
@@ -1118,6 +1145,16 @@ const PersonalSetting = () => {
>
<div style={{ marginTop: 20 }}>
<Input
name='original_password'
placeholder={t('原密码')}
type='password'
value={inputs.original_password}
onChange={(value) =>
handleInputChange('original_password', value)
}
/>
<Input
style={{ marginTop: 20 }}
name='set_new_password'
placeholder={t('新密码')}
value={inputs.set_new_password}

View File

@@ -13,6 +13,7 @@ const RateLimitSetting = () => {
ModelRequestRateLimitCount: 0,
ModelRequestRateLimitSuccessCount: 1000,
ModelRequestRateLimitDurationMinutes: 1,
ModelRequestRateLimitGroup: '',
});
let [loading, setLoading] = useState(false);
@@ -23,10 +24,14 @@ const RateLimitSetting = () => {
if (success) {
let newInputs = {};
data.forEach((item) => {
if (item.key.endsWith('Enabled')) {
newInputs[item.key] = item.value === 'true' ? true : false;
} else {
newInputs[item.key] = item.value;
if (item.key === 'ModelRequestRateLimitGroup') {
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
}
if (item.key.endsWith('Enabled')) {
newInputs[item.key] = item.value === 'true' ? true : false;
} else {
newInputs[item.key] = item.value;
}
});

View File

@@ -19,7 +19,7 @@ import {
verifyJSON,
} from '../helpers/utils';
import { API } from '../helpers/api';
import axios from "axios";
import axios from 'axios';
const SystemSetting = () => {
let [inputs, setInputs] = useState({
@@ -45,6 +45,7 @@ const SystemSetting = () => {
ServerAddress: '',
WorkerUrl: '',
WorkerValidKey: '',
WorkerAllowHttpImageRequestEnabled: '',
EpayId: '',
EpayKey: '',
Price: 7.3,
@@ -111,6 +112,7 @@ const SystemSetting = () => {
case 'SMTPSSLEnabled':
case 'LinuxDOOAuthEnabled':
case 'oidc.enabled':
case 'WorkerAllowHttpImageRequestEnabled':
item.value = item.value === 'true';
break;
case 'Price':
@@ -206,7 +208,11 @@ const SystemSetting = () => {
let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
const options = [
{ key: 'WorkerUrl', value: WorkerUrl },
]
{
key: 'WorkerAllowHttpImageRequestEnabled',
value: inputs.WorkerAllowHttpImageRequestEnabled ? 'true' : 'false',
},
];
if (inputs.WorkerValidKey !== '' || WorkerUrl === '') {
options.push({ key: 'WorkerValidKey', value: inputs.WorkerValidKey });
}
@@ -302,7 +308,8 @@ const SystemSetting = () => {
const domain = emailToAdd.trim();
// 验证域名格式
const domainRegex = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
const domainRegex =
/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
if (!domainRegex.test(domain)) {
showError('邮箱域名格式不正确,请输入有效的域名,如 gmail.com');
return;
@@ -577,6 +584,12 @@ const SystemSetting = () => {
/>
</Col>
</Row>
<Form.Checkbox
field='WorkerAllowHttpImageRequestEnabled'
noLabel
>
允许 HTTP 协议图片请求适用于自部署代理
</Form.Checkbox>
<Button onClick={submitWorker}>更新Worker设置</Button>
</Form.Section>
</Card>
@@ -799,7 +812,13 @@ const SystemSetting = () => {
onChange={(value) => setEmailToAdd(value)}
style={{ marginTop: 16 }}
suffix={
<Button theme="solid" type="primary" onClick={handleAddEmail}>添加</Button>
<Button
theme='solid'
type='primary'
onClick={handleAddEmail}
>
添加
</Button>
}
onEnterPress={handleAddEmail}
/>

View File

@@ -118,6 +118,11 @@ export const CHANNEL_OPTIONS = [
{
value: 48,
color: 'blue',
label: 'xAI'
}
label: 'xAI',
},
{
value: 49,
color: 'blue',
label: 'Coze',
},
];

View File

@@ -317,6 +317,12 @@ export function renderModelPrice(
image = false,
imageRatio = 1.0,
imageOutputTokens = 0,
webSearch = false,
webSearchCallCount = 0,
webSearchPrice = 0,
fileSearch = false,
fileSearchCallCount = 0,
fileSearchPrice = 0,
) {
if (modelPrice !== -1) {
return i18next.t(
@@ -339,14 +345,17 @@ export function renderModelPrice(
// Calculate effective input tokens (non-cached + cached with ratio applied)
let effectiveInputTokens =
inputTokens - cacheTokens + cacheTokens * cacheRatio;
// Handle image tokens if present
// Handle image tokens if present
if (image && imageOutputTokens > 0) {
effectiveInputTokens = inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
effectiveInputTokens =
inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
}
let price =
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
(completionTokens / 1000000) * completionRatioPrice * groupRatio;
(completionTokens / 1000000) * completionRatioPrice * groupRatio +
(webSearchCallCount / 1000) * webSearchPrice * groupRatio +
(fileSearchCallCount / 1000) * fileSearchPrice * groupRatio;
return (
<>
@@ -391,9 +400,23 @@ export function renderModelPrice(
)}
</p>
)}
{webSearch && webSearchCallCount > 0 && (
<p>
{i18next.t('Web搜索价格${{price}} / 1K 次', {
price: webSearchPrice,
})}
</p>
)}
{fileSearch && fileSearchCallCount > 0 && (
<p>
{i18next.t('文件搜索价格:${{price}} / 1K 次', {
price: fileSearchPrice,
})}
</p>
)}
<p></p>
<p>
{cacheTokens > 0 && !image
{cacheTokens > 0 && !image && !webSearch && !fileSearch
? i18next.t(
'输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
{
@@ -407,31 +430,82 @@ export function renderModelPrice(
total: price.toFixed(6),
},
)
: image && imageOutputTokens > 0
? i18next.t(
'输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
{
nonImageInput: inputTokens - imageOutputTokens,
imageInput: imageOutputTokens,
imageRatio: imageRatio,
price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice,
ratio: groupRatio,
total: price.toFixed(6),
},
)
: i18next.t(
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
{
input: inputTokens,
price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice,
ratio: groupRatio,
total: price.toFixed(6),
},
)}
: image && imageOutputTokens > 0 && !webSearch && !fileSearch
? i18next.t(
'输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
{
nonImageInput: inputTokens - imageOutputTokens,
imageInput: imageOutputTokens,
imageRatio: imageRatio,
price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice,
ratio: groupRatio,
total: price.toFixed(6),
},
)
: webSearch && webSearchCallCount > 0 && !image && !fileSearch
? i18next.t(
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}} = ${{total}}',
{
input: inputTokens,
price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice,
ratio: groupRatio,
webSearchCallCount,
webSearchPrice,
total: price.toFixed(6),
},
)
: fileSearch &&
fileSearchCallCount > 0 &&
!image &&
!webSearch
? i18next.t(
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
{
input: inputTokens,
price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice,
ratio: groupRatio,
fileSearchCallCount,
fileSearchPrice,
total: price.toFixed(6),
},
)
: webSearch &&
webSearchCallCount > 0 &&
fileSearch &&
fileSearchCallCount > 0 &&
!image
? i18next.t(
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}}+ 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
{
input: inputTokens,
price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice,
ratio: groupRatio,
webSearchCallCount,
webSearchPrice,
fileSearchCallCount,
fileSearchPrice,
total: price.toFixed(6),
},
)
: i18next.t(
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
{
input: inputTokens,
price: inputRatioPrice,
completion: completionTokens,
compPrice: completionRatioPrice,
ratio: groupRatio,
total: price.toFixed(6),
},
)}
</p>
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
</article>
@@ -448,33 +522,56 @@ export function renderLogContent(
user_group_ratio,
image = false,
imageRatio = 1.0,
useUserGroupRatio = undefined
useUserGroupRatio = undefined,
webSearch = false,
webSearchCallCount = 0,
fileSearch = false,
fileSearchCallCount = 0,
) {
const ratioLabel = useUserGroupRatio ? i18next.t('专属倍率') : i18next.t('分组倍率');
const ratioLabel = useUserGroupRatio
? i18next.t('专属倍率')
: i18next.t('分组倍率');
const ratio = useUserGroupRatio ? user_group_ratio : groupRatio;
if (modelPrice !== -1) {
return i18next.t('模型价格 ${{price}}{{ratioType}} {{ratio}}', {
price: modelPrice,
ratioType: ratioLabel,
ratio
ratio,
});
} else {
if (image) {
return i18next.t('模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}}{{ratioType}} {{ratio}}', {
modelRatio: modelRatio,
completionRatio: completionRatio,
imageRatio: imageRatio,
ratioType: ratioLabel,
ratio
});
return i18next.t(
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}}{{ratioType}} {{ratio}}',
{
modelRatio: modelRatio,
completionRatio: completionRatio,
imageRatio: imageRatio,
ratioType: ratioLabel,
ratio,
},
);
} else if (webSearch) {
return i18next.t(
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}}{{ratioType}} {{ratio}}Web 搜索调用 {{webSearchCallCount}} 次',
{
modelRatio: modelRatio,
completionRatio: completionRatio,
ratioType: ratioLabel,
ratio,
webSearchCallCount,
},
);
} else {
return i18next.t('模型倍率 {{modelRatio}},输出倍率 {{completionRatio}}{{ratioType}} {{ratio}}', {
modelRatio: modelRatio,
completionRatio: completionRatio,
ratioType: ratioLabel,
ratio
});
return i18next.t(
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}}{{ratioType}} {{ratio}}',
{
modelRatio: modelRatio,
completionRatio: completionRatio,
ratioType: ratioLabel,
ratio,
},
);
}
}
}

View File

@@ -493,6 +493,7 @@
"默认": "default",
"图片演示": "Image demo",
"注意系统请求的时模型名称中的点会被剔除例如gpt-4.1会请求为gpt-41所以在Azure部署的时候部署模型名称需要手动改为gpt-41": "Note that the dot in the model name requested by the system will be removed, for example: gpt-4.1 will be requested as gpt-41, so when deploying on Azure, the deployment model name needs to be manually changed to gpt-41",
"2025年5月10日后添加的渠道不需要再在部署的时候移除模型名称中的\".\"": "After May 10, 2025, channels added do not need to remove the dot in the model name during deployment",
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
"取消无限额度": "Cancel unlimited quota",
"取消": "Cancel",
@@ -1085,7 +1086,7 @@
"没有账户?": "No account? ",
"请输入 AZURE_OPENAI_ENDPOINT例如https://docs-test-001.openai.azure.com": "Please enter AZURE_OPENAI_ENDPOINT, e.g.: https://docs-test-001.openai.azure.com",
"默认 API 版本": "Default API Version",
"请输入默认 API 版本例如2024-12-01-preview": "Please enter default API version, e.g.: 2024-12-01-preview.",
"请输入默认 API 版本例如2025-04-01-preview": "Please enter default API version, e.g.: 2025-04-01-preview.",
"请为渠道命名": "Please name the channel",
"请选择可以使用该渠道的分组": "Please select groups that can use this channel",
"请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit Group ratios in system settings to add new groups:",
@@ -1373,4 +1374,4 @@
"适用于展示系统功能的场景。": "Suitable for scenarios where the system functions are displayed.",
"可在初始化后修改": "Can be modified after initialization",
"初始化系统": "Initialize system"
}
}

View File

@@ -24,7 +24,8 @@ import {
TextArea,
Checkbox,
Banner,
Modal, ImagePreview
Modal,
ImagePreview,
} from '@douyinfe/semi-ui';
import { getChannelModels, loadChannelModels } from '../../components/utils.js';
import { IconHelpCircle } from '@douyinfe/semi-icons';
@@ -306,7 +307,7 @@ const EditChannel = (props) => {
fetchModels().then();
fetchGroups().then();
if (isEdit) {
loadChannel().then(() => { });
loadChannel().then(() => {});
} else {
setInputs(originInputs);
let localModels = getChannelModels(inputs.type);
@@ -477,24 +478,26 @@ const EditChannel = (props) => {
type={'warning'}
description={
<>
{t('注意系统请求的时模型名称中的点会被剔除例如gpt-4.1会请求为gpt-41所以在Azure部署的时候部署模型名称需要手动改为gpt-41')}
<br />
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer',
}}
onClick={() => {
setModalImageUrl(
'/azure_model_name.png',
);
setIsModalOpenurl(true)
{t(
'2025年5月10日后添加的渠道不需要再在部署的时候移除模型名称中的"."',
)}
{/*<br />*/}
{/*<Typography.Text*/}
{/* style={{*/}
{/* color: 'rgba(var(--semi-blue-5), 1)',*/}
{/* userSelect: 'none',*/}
{/* cursor: 'pointer',*/}
{/* }}*/}
{/* onClick={() => {*/}
{/* setModalImageUrl(*/}
{/* '/azure_model_name.png',*/}
{/* );*/}
{/* setIsModalOpenurl(true)*/}
}}
>
{t('查看示例')}
</Typography.Text>
{/* }}*/}
{/*>*/}
{/* {t('查看示例')}*/}
{/*</Typography.Text>*/}
</>
}
></Banner>
@@ -522,7 +525,7 @@ const EditChannel = (props) => {
<Input
label={t('默认 API 版本')}
name='azure_other'
placeholder={t('请输入默认 API 版本例如2024-12-01-preview')}
placeholder={t('请输入默认 API 版本例如2025-04-01-preview')}
onChange={(value) => {
handleInputChange('other', value);
}}
@@ -584,25 +587,35 @@ const EditChannel = (props) => {
value={inputs.name}
autoComplete='new-password'
/>
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('API地址')}</Typography.Text>
</div>
<Tooltip content={t('对于官方渠道new-api已经内置地址除非是第三方代理站点或者Azure的特殊接入地址否则不需要填写')}>
<Input
label={t('API地址')}
name="base_url"
placeholder={t('此项可选用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/')}
onChange={(value) => {
handleInputChange('base_url', value);
}}
value={inputs.base_url}
autoComplete="new-password"
/>
</Tooltip>
</>
)}
{inputs.type !== 3 &&
inputs.type !== 8 &&
inputs.type !== 22 &&
inputs.type !== 36 &&
inputs.type !== 45 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('API地址')}</Typography.Text>
</div>
<Tooltip
content={t(
'对于官方渠道new-api已经内置地址除非是第三方代理站点或者Azure的特殊接入地址否则不需要填写',
)}
>
<Input
label={t('API地址')}
name='base_url'
placeholder={t(
'此项可选用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/',
)}
onChange={(value) => {
handleInputChange('base_url', value);
}}
value={inputs.base_url}
autoComplete='new-password'
/>
</Tooltip>
</>
)}
<div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('密钥')}</Typography.Text>
</div>
@@ -761,10 +774,10 @@ const EditChannel = (props) => {
name='other'
placeholder={t(
'请输入部署地区例如us-central1\n支持使用模型映射格式\n' +
'{\n' +
' "default": "us-central1",\n' +
' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
'}',
'{\n' +
' "default": "us-central1",\n' +
' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
'}',
)}
autosize={{ minRows: 2 }}
onChange={(value) => {
@@ -825,6 +838,22 @@ const EditChannel = (props) => {
/>
</>
)}
{inputs.type === 49 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>智能体ID</Typography.Text>
</div>
<Input
name='other'
placeholder={'请输入智能体ID例如7342866812345'}
onChange={(value) => {
handleInputChange('other', value);
}}
value={inputs.other}
autoComplete='new-password'
/>
</>
)}
<div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('模型')}</Typography.Text>
</div>

View File

@@ -6,6 +6,7 @@ import {
showError,
showSuccess,
showWarning,
verifyJSON,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
@@ -18,6 +19,7 @@ export default function RequestRateLimit(props) {
ModelRequestRateLimitCount: -1,
ModelRequestRateLimitSuccessCount: 1000,
ModelRequestRateLimitDurationMinutes: 1,
ModelRequestRateLimitGroup: '',
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
@@ -46,6 +48,13 @@ export default function RequestRateLimit(props) {
if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
}
for (let i = 0; i < res.length; i++) {
if (!res[i].data.success) {
return showError(res[i].data.message);
}
}
showSuccess(t('保存成功'));
props.refresh();
})
@@ -147,6 +156,41 @@ export default function RequestRateLimit(props) {
/>
</Col>
</Row>
<Row>
<Col xs={24} sm={16}>
<Form.TextArea
label={t('分组速率限制')}
placeholder={t(
'{\n "default": [200, 100],\n "vip": [0, 1000]\n}',
)}
field={'ModelRequestRateLimitGroup'}
autosize={{ minRows: 5, maxRows: 15 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: t('不是合法的 JSON 字符串'),
},
]}
extraText={
<div>
<p style={{ marginBottom: -15 }}>{t('说明:')}</p>
<ul>
<li>{t('使用 JSON 对象格式,格式为:{"组名": [最多请求次数, 最多请求完成次数]}')}</li>
<li>{t('示例:{"default": [200, 100], "vip": [0, 1000]}。')}</li>
<li>{t('[最多请求次数]必须大于等于0[最多请求完成次数]必须大于等于1。')}</li>
<li>{t('分组速率配置优先级高于全局速率限制。')}</li>
<li>{t('限制周期统一使用上方配置的“限制周期”值。')}</li>
</ul>
</div>
}
onChange={(value) => {
setInputs({ ...inputs, ModelRequestRateLimitGroup: value });
}}
/>
</Col>
</Row>
<Row>
<Button size='default' onClick={onSubmit}>
{t('保存模型速率限制')}