one-api/web/src/views/Setting/component/OperationSetting.js
2024-04-27 20:50:36 +08:00

576 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useContext } from 'react';
import SubCard from 'ui-component/cards/SubCard';
import { Stack, FormControl, InputLabel, OutlinedInput, Checkbox, Button, FormControlLabel, TextField, Alert } from '@mui/material';
import { showSuccess, showError, verifyJSON } from 'utils/common';
import { API } from 'utils/api';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import ChatLinksDataGrid from './ChatLinksDataGrid';
import dayjs from 'dayjs';
import { LoadStatusContext } from 'contexts/StatusContext';
require('dayjs/locale/zh-cn');
const OperationSetting = () => {
let now = new Date();
let [inputs, setInputs] = useState({
QuotaForNewUser: 0,
QuotaForInviter: 0,
QuotaForInvitee: 0,
QuotaRemindThreshold: 0,
PreConsumedQuota: 0,
GroupRatio: '',
TopUpLink: '',
ChatLink: '',
ChatLinks: '',
QuotaPerUnit: 0,
AutomaticDisableChannelEnabled: '',
AutomaticEnableChannelEnabled: '',
ChannelDisableThreshold: 0,
LogConsumeEnabled: '',
DisplayInCurrencyEnabled: '',
DisplayTokenStatEnabled: '',
ApproximateTokenEnabled: '',
RetryTimes: 0,
RetryCooldownSeconds: 0,
MjNotifyEnabled: '',
ChatCacheEnabled: '',
ChatCacheExpireMinute: 5
});
const [originInputs, setOriginInputs] = useState({});
let [loading, setLoading] = useState(false);
let [historyTimestamp, setHistoryTimestamp] = useState(now.getTime() / 1000 - 30 * 24 * 3600); // a month ago new Date().getTime() / 1000 + 3600
const loadStatus = useContext(LoadStatusContext);
const getOptions = async () => {
try {
const res = await API.get('/api/option/');
const { success, message, data } = res.data;
if (success) {
let newInputs = {};
data.forEach((item) => {
if (item.key === 'GroupRatio') {
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
}
newInputs[item.key] = item.value;
});
setInputs(newInputs);
setOriginInputs(newInputs);
} else {
showError(message);
}
} catch (error) {
return;
}
};
useEffect(() => {
getOptions().then();
}, []);
const updateOption = async (key, value) => {
setLoading(true);
if (key.endsWith('Enabled')) {
value = inputs[key] === 'true' ? 'false' : 'true';
}
try {
const res = await API.put('/api/option/', {
key,
value
});
const { success, message } = res.data;
if (success) {
setInputs((inputs) => ({ ...inputs, [key]: value }));
getOptions();
await loadStatus();
} else {
showError(message);
}
} catch (error) {
return;
}
setLoading(false);
};
const handleInputChange = async (event) => {
let { name, value } = event.target;
if (name.endsWith('Enabled')) {
await updateOption(name, value);
showSuccess('设置成功!');
} 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['GroupRatio'] !== inputs.GroupRatio) {
if (!verifyJSON(inputs.GroupRatio)) {
showError('分组倍率不是合法的 JSON 字符串');
return;
}
await updateOption('GroupRatio', inputs.GroupRatio);
}
break;
case 'chatlinks':
if (originInputs['ChatLinks'] !== inputs.ChatLinks) {
if (!verifyJSON(inputs.ChatLinks)) {
showError('links不是合法的 JSON 字符串');
return;
}
await updateOption('ChatLinks', inputs.ChatLinks);
}
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 (inputs.QuotaPerUnit < 0 || inputs.RetryTimes < 0 || inputs.RetryCooldownSeconds < 0) {
showError('单位额度、重试次数、冷却时间不能为负数');
return;
}
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);
}
if (originInputs['RetryCooldownSeconds'] !== inputs.RetryCooldownSeconds) {
await updateOption('RetryCooldownSeconds', inputs.RetryCooldownSeconds);
}
break;
case 'other':
if (originInputs['ChatCacheExpireMinute'] !== inputs.ChatCacheExpireMinute) {
await updateOption('ChatCacheExpireMinute', inputs.ChatCacheExpireMinute);
}
break;
}
showSuccess('保存成功!');
};
const deleteHistoryLogs = async () => {
try {
const res = await API.delete(`/api/log/?target_timestamp=${Math.floor(historyTimestamp)}`);
const { success, message, data } = res.data;
if (success) {
showSuccess(`${data} 条日志已清理!`);
return;
}
showError('日志清理失败:' + message);
} catch (error) {
return;
}
};
return (
<Stack spacing={2}>
<SubCard title="通用设置">
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
<Stack direction={{ sm: 'column', md: 'row' }} spacing={{ xs: 3, sm: 2, md: 4 }}>
<FormControl fullWidth>
<InputLabel htmlFor="TopUpLink">充值链接</InputLabel>
<OutlinedInput
id="TopUpLink"
name="TopUpLink"
value={inputs.TopUpLink}
onChange={handleInputChange}
label="充值链接"
placeholder="例如发卡网站的购买链接"
disabled={loading}
/>
</FormControl>
<FormControl fullWidth>
<InputLabel htmlFor="ChatLink">聊天链接</InputLabel>
<OutlinedInput
id="ChatLink"
name="ChatLink"
value={inputs.ChatLink}
onChange={handleInputChange}
label="聊天链接"
placeholder="例如 ChatGPT Next Web 的部署地址"
disabled={loading}
/>
</FormControl>
<FormControl fullWidth>
<InputLabel htmlFor="QuotaPerUnit">单位额度</InputLabel>
<OutlinedInput
id="QuotaPerUnit"
name="QuotaPerUnit"
value={inputs.QuotaPerUnit}
onChange={handleInputChange}
label="单位额度"
placeholder="一单位货币能兑换的额度"
disabled={loading}
/>
</FormControl>
<FormControl fullWidth>
<InputLabel htmlFor="RetryTimes">重试次数</InputLabel>
<OutlinedInput
id="RetryTimes"
name="RetryTimes"
value={inputs.RetryTimes}
onChange={handleInputChange}
label="重试次数"
placeholder="重试次数"
disabled={loading}
/>
</FormControl>
<FormControl fullWidth>
<InputLabel htmlFor="RetryCooldownSeconds">重试间隔()</InputLabel>
<OutlinedInput
id="RetryCooldownSeconds"
name="RetryCooldownSeconds"
value={inputs.RetryCooldownSeconds}
onChange={handleInputChange}
label="重试间隔(秒)"
placeholder="重试间隔(秒)"
disabled={loading}
/>
</FormControl>
</Stack>
<Stack
direction={{ sm: 'column', md: 'row' }}
spacing={{ xs: 3, sm: 2, md: 4 }}
justifyContent="flex-start"
alignItems="flex-start"
>
<FormControlLabel
sx={{ marginLeft: '0px' }}
label="以货币形式显示额度"
control={
<Checkbox
checked={inputs.DisplayInCurrencyEnabled === 'true'}
onChange={handleInputChange}
name="DisplayInCurrencyEnabled"
/>
}
/>
<FormControlLabel
label="Billing 相关 API 显示令牌额度而非用户额度"
control={
<Checkbox checked={inputs.DisplayTokenStatEnabled === 'true'} onChange={handleInputChange} name="DisplayTokenStatEnabled" />
}
/>
<FormControlLabel
label="使用近似的方式估算 token 数以减少计算量"
control={
<Checkbox checked={inputs.ApproximateTokenEnabled === 'true'} onChange={handleInputChange} name="ApproximateTokenEnabled" />
}
/>
</Stack>
<Button
variant="contained"
onClick={() => {
submitConfig('general').then();
}}
>
保存通用设置
</Button>
</Stack>
</SubCard>
<SubCard title="其他设置">
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
<Stack
direction={{ sm: 'column', md: 'row' }}
spacing={{ xs: 3, sm: 2, md: 4 }}
justifyContent="flex-start"
alignItems="flex-start"
>
<FormControlLabel
sx={{ marginLeft: '0px' }}
label="Midjourney 允许回调会泄露服务器ip地址"
control={<Checkbox checked={inputs.MjNotifyEnabled === 'true'} onChange={handleInputChange} name="MjNotifyEnabled" />}
/>
<FormControlLabel
sx={{ marginLeft: '0px' }}
label="是否开启聊天缓存(如果没有启用Redis将会存储在数据库中)"
control={<Checkbox checked={inputs.ChatCacheEnabled === 'true'} onChange={handleInputChange} name="ChatCacheEnabled" />}
/>
</Stack>
<Stack direction={{ sm: 'column', md: 'row' }} spacing={{ xs: 3, sm: 2, md: 4 }}>
<FormControl>
<InputLabel htmlFor="ChatCacheExpireMinute">缓存时间(分钟)</InputLabel>
<OutlinedInput
id="ChatCacheExpireMinute"
name="ChatCacheExpireMinute"
value={inputs.ChatCacheExpireMinute}
onChange={handleInputChange}
label="缓存时间(分钟)"
placeholder="开启缓存时,数据缓存的时间"
disabled={loading}
/>
</FormControl>
</Stack>
<Button
variant="contained"
onClick={() => {
submitConfig('other').then();
}}
>
保存其他设置
</Button>
</Stack>
</SubCard>
<SubCard title="日志设置">
<Stack direction="column" justifyContent="flex-start" alignItems="flex-start" spacing={2}>
<FormControlLabel
label="启用日志消费"
control={<Checkbox checked={inputs.LogConsumeEnabled === 'true'} onChange={handleInputChange} name="LogConsumeEnabled" />}
/>
<FormControl>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={'zh-cn'}>
<DateTimePicker
label="日志清理时间"
placeholder="日志清理时间"
ampm={false}
name="historyTimestamp"
value={historyTimestamp === null ? null : dayjs.unix(historyTimestamp)}
disabled={loading}
onChange={(newValue) => {
setHistoryTimestamp(newValue === null ? null : newValue.unix());
}}
slotProps={{
actionBar: {
actions: ['today', 'clear', 'accept']
}
}}
/>
</LocalizationProvider>
</FormControl>
<Button
variant="contained"
onClick={() => {
deleteHistoryLogs().then();
}}
>
清理历史日志
</Button>
</Stack>
</SubCard>
<SubCard title="监控设置">
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
<Stack direction={{ sm: 'column', md: 'row' }} spacing={{ xs: 3, sm: 2, md: 4 }}>
<FormControl fullWidth>
<InputLabel htmlFor="ChannelDisableThreshold">最长响应时间</InputLabel>
<OutlinedInput
id="ChannelDisableThreshold"
name="ChannelDisableThreshold"
type="number"
value={inputs.ChannelDisableThreshold}
onChange={handleInputChange}
label="最长响应时间"
placeholder="单位秒,当运行通道全部测试时,超过此时间将自动禁用通道"
disabled={loading}
/>
</FormControl>
<FormControl fullWidth>
<InputLabel htmlFor="QuotaRemindThreshold">额度提醒阈值</InputLabel>
<OutlinedInput
id="QuotaRemindThreshold"
name="QuotaRemindThreshold"
type="number"
value={inputs.QuotaRemindThreshold}
onChange={handleInputChange}
label="额度提醒阈值"
placeholder="低于此额度时将发送邮件提醒用户"
disabled={loading}
/>
</FormControl>
</Stack>
<FormControlLabel
label="失败时自动禁用通道"
control={
<Checkbox
checked={inputs.AutomaticDisableChannelEnabled === 'true'}
onChange={handleInputChange}
name="AutomaticDisableChannelEnabled"
/>
}
/>
<FormControlLabel
label="成功时自动启用通道"
control={
<Checkbox
checked={inputs.AutomaticEnableChannelEnabled === 'true'}
onChange={handleInputChange}
name="AutomaticEnableChannelEnabled"
/>
}
/>
<Button
variant="contained"
onClick={() => {
submitConfig('monitor').then();
}}
>
保存监控设置
</Button>
</Stack>
</SubCard>
<SubCard title="额度设置">
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
<Stack direction={{ sm: 'column', md: 'row' }} spacing={{ xs: 3, sm: 2, md: 4 }}>
<FormControl fullWidth>
<InputLabel htmlFor="QuotaForNewUser">新用户初始额度</InputLabel>
<OutlinedInput
id="QuotaForNewUser"
name="QuotaForNewUser"
type="number"
value={inputs.QuotaForNewUser}
onChange={handleInputChange}
label="新用户初始额度"
placeholder="例如100"
disabled={loading}
/>
</FormControl>
<FormControl fullWidth>
<InputLabel htmlFor="PreConsumedQuota">请求预扣费额度</InputLabel>
<OutlinedInput
id="PreConsumedQuota"
name="PreConsumedQuota"
type="number"
value={inputs.PreConsumedQuota}
onChange={handleInputChange}
label="请求预扣费额度"
placeholder="请求结束后多退少补"
disabled={loading}
/>
</FormControl>
<FormControl fullWidth>
<InputLabel htmlFor="QuotaForInviter">邀请新用户奖励额度</InputLabel>
<OutlinedInput
id="QuotaForInviter"
name="QuotaForInviter"
type="number"
label="邀请新用户奖励额度"
value={inputs.QuotaForInviter}
onChange={handleInputChange}
placeholder="例如2000"
disabled={loading}
/>
</FormControl>
<FormControl fullWidth>
<InputLabel htmlFor="QuotaForInvitee">新用户使用邀请码奖励额度</InputLabel>
<OutlinedInput
id="QuotaForInvitee"
name="QuotaForInvitee"
type="number"
label="新用户使用邀请码奖励额度"
value={inputs.QuotaForInvitee}
onChange={handleInputChange}
autoComplete="new-password"
placeholder="例如1000"
disabled={loading}
/>
</FormControl>
</Stack>
<Button
variant="contained"
onClick={() => {
submitConfig('quota').then();
}}
>
保存额度设置
</Button>
</Stack>
</SubCard>
<SubCard title="倍率设置">
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
<FormControl fullWidth>
<TextField
multiline
maxRows={15}
id="channel-GroupRatio-label"
label="分组倍率"
value={inputs.GroupRatio}
name="GroupRatio"
onChange={handleInputChange}
aria-describedby="helper-text-channel-GroupRatio-label"
minRows={5}
placeholder="为一个 JSON 文本,键为分组名称,值为倍率"
/>
</FormControl>
<Button
variant="contained"
onClick={() => {
submitConfig('ratio').then();
}}
>
保存倍率设置
</Button>
</Stack>
</SubCard>
<SubCard title="聊天链接设置">
<Stack spacing={2}>
<Alert severity="info">
配置聊天链接该配置在令牌中的聊天生效以及首页的Playground中的聊天生效. <br />
链接中可以使{'{key}'}替换用户的令牌{'{server}'}替换服务器地址例如
{'https://chat.oneapi.pro/#/?settings={"key":"sk-{key}","url":"{server}"}'}
<br />
如果未配置会默认配置以下4个链接
<br />
ChatGPT Next {'https://chat.oneapi.pro/#/?settings={"key":"{key}","url":"{server}"}'}
<br />
chatgpt-web-midjourney-proxy {'https://vercel.ddaiai.com/#/?settings={"key":"{key}","url":"{server}"}'}
<br />
AMA 问天 {'ama://set-api-key?server={server}&key={key}'}
<br />
opencat {'opencat://team/join?domain={server}&token={key}'}
<br />
</Alert>
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
<ChatLinksDataGrid links={inputs.ChatLinks || '[]'} onChange={handleInputChange} />
<Button
variant="contained"
onClick={() => {
submitConfig('chatlinks').then();
}}
>
保存聊天链接设置
</Button>
</Stack>
</Stack>
</SubCard>
</Stack>
);
};
export default OperationSetting;