mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-12 03:13:41 +08:00
447 lines
15 KiB
JavaScript
447 lines
15 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Divider, Form, Grid, Header } from 'semantic-ui-react';
|
|
import {
|
|
API,
|
|
showError,
|
|
showSuccess,
|
|
timestamp2string,
|
|
verifyJSON,
|
|
} from '../helpers';
|
|
|
|
const OperationSetting = () => {
|
|
const { t } = useTranslation();
|
|
let now = new Date();
|
|
let [inputs, setInputs] = useState({
|
|
QuotaForNewUser: 0,
|
|
QuotaForInviter: 0,
|
|
QuotaForInvitee: 0,
|
|
QuotaRemindThreshold: 0,
|
|
PreConsumedQuota: 0,
|
|
ModelRatio: '',
|
|
CompletionRatio: '',
|
|
GroupRatio: '',
|
|
TopUpLink: '',
|
|
ChatLink: '',
|
|
QuotaPerUnit: 0,
|
|
AutomaticDisableChannelEnabled: '',
|
|
AutomaticEnableChannelEnabled: '',
|
|
ChannelDisableThreshold: 0,
|
|
LogConsumeEnabled: '',
|
|
DisplayInCurrencyEnabled: '',
|
|
DisplayTokenStatEnabled: '',
|
|
ApproximateTokenEnabled: '',
|
|
RetryTimes: 0,
|
|
});
|
|
const [originInputs, setOriginInputs] = useState({});
|
|
let [loading, setLoading] = useState(false);
|
|
let [historyTimestamp, setHistoryTimestamp] = useState(
|
|
timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600)
|
|
); // a month ago
|
|
|
|
const getOptions = async () => {
|
|
const res = await API.get('/api/option/');
|
|
const { success, message, data } = res.data;
|
|
if (success) {
|
|
let newInputs = {};
|
|
data.forEach((item) => {
|
|
if (
|
|
item.key === 'ModelRatio' ||
|
|
item.key === 'GroupRatio' ||
|
|
item.key === 'CompletionRatio'
|
|
) {
|
|
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
|
}
|
|
if (item.value === '{}') {
|
|
item.value = '';
|
|
}
|
|
newInputs[item.key] = item.value;
|
|
});
|
|
setInputs(newInputs);
|
|
setOriginInputs(newInputs);
|
|
} else {
|
|
showError(message);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
getOptions().then();
|
|
}, []);
|
|
|
|
const updateOption = async (key, value) => {
|
|
setLoading(true);
|
|
if (key.endsWith('Enabled')) {
|
|
value = inputs[key] === 'true' ? 'false' : 'true';
|
|
}
|
|
const res = await API.put('/api/option/', {
|
|
key,
|
|
value,
|
|
});
|
|
const { success, message } = res.data;
|
|
if (success) {
|
|
setInputs((inputs) => ({ ...inputs, [key]: value }));
|
|
} else {
|
|
showError(message);
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
const handleInputChange = async (e, { name, value }) => {
|
|
if (name.endsWith('Enabled')) {
|
|
await updateOption(name, value);
|
|
} else {
|
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
|
}
|
|
};
|
|
|
|
const submitConfig = async (group) => {
|
|
switch (group) {
|
|
case 'monitor':
|
|
if (
|
|
originInputs['ChannelDisableThreshold'] !==
|
|
inputs.ChannelDisableThreshold
|
|
) {
|
|
await updateOption(
|
|
'ChannelDisableThreshold',
|
|
inputs.ChannelDisableThreshold
|
|
);
|
|
}
|
|
if (
|
|
originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold
|
|
) {
|
|
await updateOption(
|
|
'QuotaRemindThreshold',
|
|
inputs.QuotaRemindThreshold
|
|
);
|
|
}
|
|
break;
|
|
case 'ratio':
|
|
if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
|
|
if (!verifyJSON(inputs.ModelRatio)) {
|
|
showError('模型倍率不是合法的 JSON 字符串');
|
|
return;
|
|
}
|
|
await updateOption('ModelRatio', inputs.ModelRatio);
|
|
}
|
|
if (originInputs['GroupRatio'] !== inputs.GroupRatio) {
|
|
if (!verifyJSON(inputs.GroupRatio)) {
|
|
showError('分组倍率不是合法的 JSON 字符串');
|
|
return;
|
|
}
|
|
await updateOption('GroupRatio', inputs.GroupRatio);
|
|
}
|
|
if (originInputs['CompletionRatio'] !== inputs.CompletionRatio) {
|
|
if (!verifyJSON(inputs.CompletionRatio)) {
|
|
showError('补全倍率不是合法的 JSON 字符串');
|
|
return;
|
|
}
|
|
await updateOption('CompletionRatio', inputs.CompletionRatio);
|
|
}
|
|
break;
|
|
case 'quota':
|
|
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
|
|
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
|
|
}
|
|
if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) {
|
|
await updateOption('QuotaForInvitee', inputs.QuotaForInvitee);
|
|
}
|
|
if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) {
|
|
await updateOption('QuotaForInviter', inputs.QuotaForInviter);
|
|
}
|
|
if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
|
|
await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
|
|
}
|
|
break;
|
|
case 'general':
|
|
if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
|
|
await updateOption('TopUpLink', inputs.TopUpLink);
|
|
}
|
|
if (originInputs['ChatLink'] !== inputs.ChatLink) {
|
|
await updateOption('ChatLink', inputs.ChatLink);
|
|
}
|
|
if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
|
|
await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
|
|
}
|
|
if (originInputs['RetryTimes'] !== inputs.RetryTimes) {
|
|
await updateOption('RetryTimes', inputs.RetryTimes);
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
const deleteHistoryLogs = async () => {
|
|
console.log(inputs);
|
|
const res = await API.delete(
|
|
`/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`
|
|
);
|
|
const { success, message, data } = res.data;
|
|
if (success) {
|
|
showSuccess(`${data} 条日志已清理!`);
|
|
return;
|
|
}
|
|
showError('日志清理失败:' + message);
|
|
};
|
|
|
|
return (
|
|
<Grid columns={1}>
|
|
<Grid.Column>
|
|
<Form loading={loading}>
|
|
<Header as='h3'>{t('setting.operation.quota.title')}</Header>
|
|
<Form.Group widths='equal'>
|
|
<Form.Input
|
|
label={t('setting.operation.quota.new_user')}
|
|
name='QuotaForNewUser'
|
|
onChange={handleInputChange}
|
|
autoComplete='new-password'
|
|
value={inputs.QuotaForNewUser}
|
|
type='number'
|
|
min='0'
|
|
placeholder={t('setting.operation.quota.new_user_placeholder')}
|
|
/>
|
|
<Form.Input
|
|
label={t('setting.operation.quota.pre_consume')}
|
|
name='PreConsumedQuota'
|
|
onChange={handleInputChange}
|
|
autoComplete='new-password'
|
|
value={inputs.PreConsumedQuota}
|
|
type='number'
|
|
min='0'
|
|
placeholder={t('setting.operation.quota.pre_consume_placeholder')}
|
|
/>
|
|
<Form.Input
|
|
label={t('setting.operation.quota.inviter_reward')}
|
|
name='QuotaForInviter'
|
|
onChange={handleInputChange}
|
|
autoComplete='new-password'
|
|
value={inputs.QuotaForInviter}
|
|
type='number'
|
|
min='0'
|
|
placeholder={t(
|
|
'setting.operation.quota.inviter_reward_placeholder'
|
|
)}
|
|
/>
|
|
<Form.Input
|
|
label={t('setting.operation.quota.invitee_reward')}
|
|
name='QuotaForInvitee'
|
|
onChange={handleInputChange}
|
|
autoComplete='new-password'
|
|
value={inputs.QuotaForInvitee}
|
|
type='number'
|
|
min='0'
|
|
placeholder={t(
|
|
'setting.operation.quota.invitee_reward_placeholder'
|
|
)}
|
|
/>
|
|
</Form.Group>
|
|
<Form.Button
|
|
onClick={() => {
|
|
submitConfig('quota').then();
|
|
}}
|
|
>
|
|
{t('setting.operation.quota.buttons.save')}
|
|
</Form.Button>
|
|
<Divider />
|
|
<Header as='h3'>{t('setting.operation.ratio.title')}</Header>
|
|
<Form.Group widths='equal'>
|
|
<Form.TextArea
|
|
label={t('setting.operation.ratio.model.title')}
|
|
name='ModelRatio'
|
|
onChange={handleInputChange}
|
|
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
|
autoComplete='new-password'
|
|
value={inputs.ModelRatio}
|
|
placeholder={t('setting.operation.ratio.model.placeholder')}
|
|
/>
|
|
</Form.Group>
|
|
<Form.Group widths='equal'>
|
|
<Form.TextArea
|
|
label={t('setting.operation.ratio.completion.title')}
|
|
name='CompletionRatio'
|
|
onChange={handleInputChange}
|
|
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
|
autoComplete='new-password'
|
|
value={inputs.CompletionRatio}
|
|
placeholder={t('setting.operation.ratio.completion.placeholder')}
|
|
/>
|
|
</Form.Group>
|
|
<Form.Group widths='equal'>
|
|
<Form.TextArea
|
|
label={t('setting.operation.ratio.group.title')}
|
|
name='GroupRatio'
|
|
onChange={handleInputChange}
|
|
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
|
autoComplete='new-password'
|
|
value={inputs.GroupRatio}
|
|
placeholder={t('setting.operation.ratio.group.placeholder')}
|
|
/>
|
|
</Form.Group>
|
|
<Form.Button
|
|
onClick={() => {
|
|
submitConfig('ratio').then();
|
|
}}
|
|
>
|
|
{t('setting.operation.ratio.buttons.save')}
|
|
</Form.Button>
|
|
<Divider />
|
|
<Header as='h3'>{t('setting.operation.log.title')}</Header>
|
|
<Form.Group inline>
|
|
<Form.Checkbox
|
|
checked={inputs.LogConsumeEnabled === 'true'}
|
|
label={t('setting.operation.log.enable_consume')}
|
|
name='LogConsumeEnabled'
|
|
onChange={handleInputChange}
|
|
/>
|
|
</Form.Group>
|
|
<Form.Group widths={4}>
|
|
<Form.Input
|
|
label={t('setting.operation.log.target_time')}
|
|
value={historyTimestamp}
|
|
type='datetime-local'
|
|
name='history_timestamp'
|
|
onChange={(e, { name, value }) => {
|
|
setHistoryTimestamp(value);
|
|
}}
|
|
/>
|
|
</Form.Group>
|
|
<Form.Button
|
|
onClick={() => {
|
|
deleteHistoryLogs().then();
|
|
}}
|
|
>
|
|
{t('setting.operation.log.buttons.clean')}
|
|
</Form.Button>
|
|
|
|
<Divider />
|
|
<Header as='h3'>{t('setting.operation.monitor.title')}</Header>
|
|
<Form.Group widths={3}>
|
|
<Form.Input
|
|
label={t('setting.operation.monitor.max_response_time')}
|
|
name='ChannelDisableThreshold'
|
|
onChange={handleInputChange}
|
|
autoComplete='new-password'
|
|
value={inputs.ChannelDisableThreshold}
|
|
type='number'
|
|
min='0'
|
|
placeholder={t(
|
|
'setting.operation.monitor.max_response_time_placeholder'
|
|
)}
|
|
/>
|
|
<Form.Input
|
|
label={t('setting.operation.monitor.quota_reminder')}
|
|
name='QuotaRemindThreshold'
|
|
onChange={handleInputChange}
|
|
autoComplete='new-password'
|
|
value={inputs.QuotaRemindThreshold}
|
|
type='number'
|
|
min='0'
|
|
placeholder={t(
|
|
'setting.operation.monitor.quota_reminder_placeholder'
|
|
)}
|
|
/>
|
|
</Form.Group>
|
|
<Form.Group inline>
|
|
<Form.Checkbox
|
|
checked={inputs.AutomaticDisableChannelEnabled === 'true'}
|
|
label={t('setting.operation.monitor.auto_disable')}
|
|
name='AutomaticDisableChannelEnabled'
|
|
onChange={handleInputChange}
|
|
/>
|
|
<Form.Checkbox
|
|
checked={inputs.AutomaticEnableChannelEnabled === 'true'}
|
|
label={t('setting.operation.monitor.auto_enable')}
|
|
name='AutomaticEnableChannelEnabled'
|
|
onChange={handleInputChange}
|
|
/>
|
|
</Form.Group>
|
|
<Form.Button
|
|
onClick={() => {
|
|
submitConfig('monitor').then();
|
|
}}
|
|
>
|
|
{t('setting.operation.monitor.buttons.save')}
|
|
</Form.Button>
|
|
|
|
<Divider />
|
|
<Header as='h3'>{t('setting.operation.general.title')}</Header>
|
|
<Form.Group widths={4}>
|
|
<Form.Input
|
|
label={t('setting.operation.general.topup_link')}
|
|
name='TopUpLink'
|
|
onChange={handleInputChange}
|
|
autoComplete='new-password'
|
|
value={inputs.TopUpLink}
|
|
type='link'
|
|
placeholder={t(
|
|
'setting.operation.general.topup_link_placeholder'
|
|
)}
|
|
/>
|
|
<Form.Input
|
|
label={t('setting.operation.general.chat_link')}
|
|
name='ChatLink'
|
|
onChange={handleInputChange}
|
|
autoComplete='new-password'
|
|
value={inputs.ChatLink}
|
|
type='link'
|
|
placeholder={t('setting.operation.general.chat_link_placeholder')}
|
|
/>
|
|
<Form.Input
|
|
label={t('setting.operation.general.quota_per_unit')}
|
|
name='QuotaPerUnit'
|
|
onChange={handleInputChange}
|
|
autoComplete='new-password'
|
|
value={inputs.QuotaPerUnit}
|
|
type='number'
|
|
step='0.01'
|
|
placeholder={t(
|
|
'setting.operation.general.quota_per_unit_placeholder'
|
|
)}
|
|
/>
|
|
<Form.Input
|
|
label={t('setting.operation.general.retry_times')}
|
|
name='RetryTimes'
|
|
type={'number'}
|
|
step='1'
|
|
min='0'
|
|
onChange={handleInputChange}
|
|
autoComplete='new-password'
|
|
value={inputs.RetryTimes}
|
|
placeholder={t(
|
|
'setting.operation.general.retry_times_placeholder'
|
|
)}
|
|
/>
|
|
</Form.Group>
|
|
<Form.Group inline>
|
|
<Form.Checkbox
|
|
checked={inputs.DisplayInCurrencyEnabled === 'true'}
|
|
label={t('setting.operation.general.display_in_currency')}
|
|
name='DisplayInCurrencyEnabled'
|
|
onChange={handleInputChange}
|
|
/>
|
|
<Form.Checkbox
|
|
checked={inputs.DisplayTokenStatEnabled === 'true'}
|
|
label={t('setting.operation.general.display_token_stat')}
|
|
name='DisplayTokenStatEnabled'
|
|
onChange={handleInputChange}
|
|
/>
|
|
<Form.Checkbox
|
|
checked={inputs.ApproximateTokenEnabled === 'true'}
|
|
label={t('setting.operation.general.approximate_token')}
|
|
name='ApproximateTokenEnabled'
|
|
onChange={handleInputChange}
|
|
/>
|
|
</Form.Group>
|
|
<Form.Button
|
|
onClick={() => {
|
|
submitConfig('general').then();
|
|
}}
|
|
>
|
|
{t('setting.operation.general.buttons.save')}
|
|
</Form.Button>
|
|
</Form>
|
|
</Grid.Column>
|
|
</Grid>
|
|
);
|
|
};
|
|
|
|
export default OperationSetting;
|