one-api/web/berry/src/views/Setting/component/OperationSetting.js
2024-03-24 23:01:03 +08:00

557 lines
18 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 } from "react";
import SubCard from "ui-component/cards/SubCard";
import {
Stack,
FormControl,
InputLabel,
OutlinedInput,
Checkbox,
Button,
FormControlLabel,
TextField,
} 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 dayjs from "dayjs";
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,
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(
now.getTime() / 1000 - 30 * 24 * 3600
); // a month ago new Date().getTime() / 1000 + 3600
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 (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["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;
}
showSuccess("保存成功!");
};
const deleteHistoryLogs = async () => {
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);
};
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>
</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
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-ModelRatio-label"
label="模型倍率"
value={inputs.ModelRatio}
name="ModelRatio"
onChange={handleInputChange}
aria-describedby="helper-text-channel-ModelRatio-label"
minRows={5}
placeholder="为一个 JSON 文本,键为模型名称,值为倍率"
/>
</FormControl>
<FormControl fullWidth>
<TextField
multiline
maxRows={15}
id="channel-CompletionRatio-label"
label="补全倍率"
value={inputs.CompletionRatio}
name="CompletionRatio"
onChange={handleInputChange}
aria-describedby="helper-text-channel-CompletionRatio-label"
minRows={5}
placeholder="为一个 JSON 文本,键为模型名称,值为倍率,此处的倍率设置是模型补全倍率相较于提示倍率的比例,使用该设置可强制覆盖 One API 的内部比例"
/>
</FormControl>
<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>
</Stack>
);
};
export default OperationSetting;