feat: Enhance user settings and notification options

This commit is contained in:
CaIon
2025-04-03 17:32:48 +08:00
parent f4cc90c8d6
commit c418d9ed9a
6 changed files with 3978 additions and 3393 deletions

View File

@@ -6,6 +6,7 @@ var (
UserSettingWebhookUrl = "webhook_url" // WebhookUrl webhook地址 UserSettingWebhookUrl = "webhook_url" // WebhookUrl webhook地址
UserSettingWebhookSecret = "webhook_secret" // WebhookSecret webhook密钥 UserSettingWebhookSecret = "webhook_secret" // WebhookSecret webhook密钥
UserSettingNotificationEmail = "notification_email" // NotificationEmail 通知邮箱地址 UserSettingNotificationEmail = "notification_email" // NotificationEmail 通知邮箱地址
UserAcceptUnsetRatioModel = "accept_unset_model_ratio_model" // AcceptUnsetRatioModel 是否接受未设置价格的模型
) )
var ( var (

View File

@@ -105,6 +105,11 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
request := buildTestRequest(testModel) request := buildTestRequest(testModel)
common.SysLog(fmt.Sprintf("testing channel %d with model %s , info %v ", channel.Id, testModel, info)) common.SysLog(fmt.Sprintf("testing channel %d with model %s , info %v ", channel.Id, testModel, info))
priceData, err := helper.ModelPriceHelper(c, info, 0, int(request.MaxTokens))
if err != nil {
return err, nil
}
adaptor.Init(info) adaptor.Init(info)
convertedRequest, err := adaptor.ConvertOpenAIRequest(c, info, request) convertedRequest, err := adaptor.ConvertOpenAIRequest(c, info, request)
@@ -143,10 +148,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
return err, nil return err, nil
} }
info.PromptTokens = usage.PromptTokens info.PromptTokens = usage.PromptTokens
priceData, err := helper.ModelPriceHelper(c, info, usage.PromptTokens, int(request.MaxTokens))
if err != nil {
return err, nil
}
quota := 0 quota := 0
if !priceData.UsePrice { if !priceData.UsePrice {
quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*priceData.CompletionRatio)) quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*priceData.CompletionRatio))

View File

@@ -918,6 +918,7 @@ type UpdateUserSettingRequest struct {
WebhookUrl string `json:"webhook_url,omitempty"` WebhookUrl string `json:"webhook_url,omitempty"`
WebhookSecret string `json:"webhook_secret,omitempty"` WebhookSecret string `json:"webhook_secret,omitempty"`
NotificationEmail string `json:"notification_email,omitempty"` NotificationEmail string `json:"notification_email,omitempty"`
AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"`
} }
func UpdateUserSetting(c *gin.Context) { func UpdateUserSetting(c *gin.Context) {
@@ -993,6 +994,7 @@ func UpdateUserSetting(c *gin.Context) {
settings := map[string]interface{}{ settings := map[string]interface{}{
constant.UserSettingNotifyType: req.QuotaWarningType, constant.UserSettingNotifyType: req.QuotaWarningType,
constant.UserSettingQuotaWarningThreshold: req.QuotaWarningThreshold, constant.UserSettingQuotaWarningThreshold: req.QuotaWarningThreshold,
"accept_unset_model_ratio_model": req.AcceptUnsetModelRatioModel,
} }
// 如果是webhook类型,添加webhook相关设置 // 如果是webhook类型,添加webhook相关设置

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"one-api/common" "one-api/common"
constant2 "one-api/constant"
relaycommon "one-api/relay/common" relaycommon "one-api/relay/common"
"one-api/setting" "one-api/setting"
"one-api/setting/operation_setting" "one-api/setting/operation_setting"
@@ -40,12 +41,21 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
var success bool var success bool
modelRatio, success = operation_setting.GetModelRatio(info.OriginModelName) modelRatio, success = operation_setting.GetModelRatio(info.OriginModelName)
if !success { if !success {
acceptUnsetRatio := false
if accept, ok := info.UserSetting[constant2.UserAcceptUnsetRatioModel]; ok {
b, ok := accept.(bool)
if ok {
acceptUnsetRatio = b
}
}
if !acceptUnsetRatio {
if info.UserId == 1 { if info.UserId == 1 {
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置请设置或开始自用模式Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName) return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置请设置或开始自用模式Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName)
} else { } else {
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置, 请联系管理员设置Model %s ratio or price not set, please contact administrator to set", info.OriginModelName, info.OriginModelName) return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置, 请联系管理员设置Model %s ratio or price not set, please contact administrator to set", info.OriginModelName, info.OriginModelName)
} }
} }
}
completionRatio = operation_setting.GetCompletionRatio(info.OriginModelName) completionRatio = operation_setting.GetCompletionRatio(info.OriginModelName)
cacheRatio, _ = operation_setting.GetCacheRatio(info.OriginModelName) cacheRatio, _ = operation_setting.GetCacheRatio(info.OriginModelName)
cacheCreationRatio, _ = operation_setting.GetCreateCacheRatio(info.OriginModelName) cacheCreationRatio, _ = operation_setting.GetCreateCacheRatio(info.OriginModelName)

5189
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,16 @@
import React, {useContext, useEffect, useState} from 'react'; import React, { useContext, useEffect, useState } from 'react';
import {useNavigate} from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { import {
API, API,
copy, copy,
isRoot, isRoot,
showError, showError,
showInfo, showInfo,
showSuccess, showSuccess
} from '../helpers'; } from '../helpers';
import Turnstile from 'react-turnstile'; import Turnstile from 'react-turnstile';
import {UserContext} from '../context/User'; import { UserContext } from '../context/User';
import {onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked} from './utils'; import { onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked } from './utils';
import { import {
Avatar, Avatar,
Banner, Banner,
@@ -30,12 +30,15 @@ import {
Radio, Radio,
RadioGroup, RadioGroup,
AutoComplete, AutoComplete,
Checkbox,
Tabs,
TabPane
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
getQuotaPerUnit, getQuotaPerUnit,
renderQuota, renderQuota,
renderQuotaWithPrompt, renderQuotaWithPrompt,
stringToColor, stringToColor
} from '../helpers/render'; } from '../helpers/render';
import TelegramLoginButton from 'react-telegram-login'; import TelegramLoginButton from 'react-telegram-login';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -51,7 +54,7 @@ const PersonalSetting = () => {
email: '', email: '',
self_account_deletion_confirmation: '', self_account_deletion_confirmation: '',
set_new_password: '', set_new_password: '',
set_new_password_confirmation: '', set_new_password_confirmation: ''
}); });
const [status, setStatus] = useState({}); const [status, setStatus] = useState({});
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false); const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
@@ -80,7 +83,8 @@ const PersonalSetting = () => {
warningThreshold: 100000, warningThreshold: 100000,
webhookUrl: '', webhookUrl: '',
webhookSecret: '', webhookSecret: '',
notificationEmail: '' notificationEmail: '',
acceptUnsetModelRatioModel: false
}); });
const [showWebhookDocs, setShowWebhookDocs] = useState(false); const [showWebhookDocs, setShowWebhookDocs] = useState(false);
@@ -123,7 +127,8 @@ const PersonalSetting = () => {
warningThreshold: settings.quota_warning_threshold || 500000, warningThreshold: settings.quota_warning_threshold || 500000,
webhookUrl: settings.webhook_url || '', webhookUrl: settings.webhook_url || '',
webhookSecret: settings.webhook_secret || '', webhookSecret: settings.webhook_secret || '',
notificationEmail: settings.notification_email || '' notificationEmail: settings.notification_email || '',
acceptUnsetModelRatioModel: settings.accept_unset_model_ratio_model || false
}); });
} }
}, [userState?.user?.setting]); }, [userState?.user?.setting]);
@@ -134,12 +139,12 @@ const PersonalSetting = () => {
}, [isModelsExpanded]); }, [isModelsExpanded]);
const handleInputChange = (name, value) => { const handleInputChange = (name, value) => {
setInputs((inputs) => ({...inputs, [name]: value})); setInputs((inputs) => ({ ...inputs, [name]: value }));
}; };
const generateAccessToken = async () => { const generateAccessToken = async () => {
const res = await API.get('/api/user/token'); const res = await API.get('/api/user/token');
const {success, message, data} = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
setSystemToken(data); setSystemToken(data);
await copy(data); await copy(data);
@@ -151,7 +156,7 @@ const PersonalSetting = () => {
const getAffLink = async () => { const getAffLink = async () => {
const res = await API.get('/api/user/aff'); const res = await API.get('/api/user/aff');
const {success, message, data} = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
let link = `${window.location.origin}/register?aff=${data}`; let link = `${window.location.origin}/register?aff=${data}`;
setAffLink(link); setAffLink(link);
@@ -162,9 +167,9 @@ const PersonalSetting = () => {
const getUserData = async () => { const getUserData = async () => {
let res = await API.get(`/api/user/self`); let res = await API.get(`/api/user/self`);
const {success, message, data} = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
userDispatch({type: 'login', payload: data}); userDispatch({ type: 'login', payload: data });
} else { } else {
showError(message); showError(message);
} }
@@ -172,7 +177,7 @@ const PersonalSetting = () => {
const loadModels = async () => { const loadModels = async () => {
let res = await API.get(`/api/user/models`); let res = await API.get(`/api/user/models`);
const {success, message, data} = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
if (data != null) { if (data != null) {
setModels(data); setModels(data);
@@ -201,12 +206,12 @@ const PersonalSetting = () => {
} }
const res = await API.delete('/api/user/self'); const res = await API.delete('/api/user/self');
const {success, message} = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess(t('账户已删除!')); showSuccess(t('账户已删除!'));
await API.get('/api/user/logout'); await API.get('/api/user/logout');
userDispatch({type: 'logout'}); userDispatch({ type: 'logout' });
localStorage.removeItem('user'); localStorage.removeItem('user');
navigate('/login'); navigate('/login');
} else { } else {
@@ -217,9 +222,9 @@ const PersonalSetting = () => {
const bindWeChat = async () => { const bindWeChat = async () => {
if (inputs.wechat_verification_code === '') return; if (inputs.wechat_verification_code === '') return;
const res = await API.get( const res = await API.get(
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`, `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
); );
const {success, message} = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess(t('微信账户绑定成功!')); showSuccess(t('微信账户绑定成功!'));
setShowWeChatBindModal(false); setShowWeChatBindModal(false);
@@ -234,9 +239,9 @@ const PersonalSetting = () => {
return; return;
} }
const res = await API.put(`/api/user/self`, { const res = await API.put(`/api/user/self`, {
password: inputs.set_new_password, password: inputs.set_new_password
}); });
const {success, message} = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess(t('密码修改成功!')); showSuccess(t('密码修改成功!'));
setShowWeChatBindModal(false); setShowWeChatBindModal(false);
@@ -252,9 +257,9 @@ const PersonalSetting = () => {
return; return;
} }
const res = await API.post(`/api/user/aff_transfer`, { const res = await API.post(`/api/user/aff_transfer`, {
quota: transferAmount, quota: transferAmount
}); });
const {success, message} = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess(message); showSuccess(message);
setOpenTransfer(false); setOpenTransfer(false);
@@ -276,9 +281,9 @@ const PersonalSetting = () => {
} }
setLoading(true); setLoading(true);
const res = await API.get( const res = await API.get(
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`, `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
); );
const {success, message} = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess(t('验证码发送成功,请检查邮箱!')); showSuccess(t('验证码发送成功,请检查邮箱!'));
} else { } else {
@@ -294,9 +299,9 @@ const PersonalSetting = () => {
} }
setLoading(true); setLoading(true);
const res = await API.get( const res = await API.get(
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`, `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
); );
const {success, message} = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess(t('邮箱账户绑定成功!')); showSuccess(t('邮箱账户绑定成功!'));
setShowEmailBindModal(false); setShowEmailBindModal(false);
@@ -324,7 +329,7 @@ const PersonalSetting = () => {
showSuccess(t('已复制:') + text); showSuccess(t('已复制:') + text);
} else { } else {
// setSearchKeyword(text); // setSearchKeyword(text);
Modal.error({title: t('无法复制到剪贴板,请手动复制'), content: text}); Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
} }
}; };
@@ -342,7 +347,8 @@ const PersonalSetting = () => {
quota_warning_threshold: parseFloat(notificationSettings.warningThreshold), quota_warning_threshold: parseFloat(notificationSettings.warningThreshold),
webhook_url: notificationSettings.webhookUrl, webhook_url: notificationSettings.webhookUrl,
webhook_secret: notificationSettings.webhookSecret, webhook_secret: notificationSettings.webhookSecret,
notification_email: notificationSettings.notificationEmail notification_email: notificationSettings.notificationEmail,
accept_unset_model_ratio_model: notificationSettings.acceptUnsetModelRatioModel
}); });
if (res.data.success) { if (res.data.success) {
@@ -370,22 +376,22 @@ const PersonalSetting = () => {
size={'small'} size={'small'}
centered={true} centered={true}
> >
<div style={{marginTop: 20}}> <div style={{ marginTop: 20 }}>
<Typography.Text>{t('可用额度')}{renderQuotaWithPrompt(userState?.user?.aff_quota)}</Typography.Text> <Typography.Text>{t('可用额度')}{renderQuotaWithPrompt(userState?.user?.aff_quota)}</Typography.Text>
<Input <Input
style={{marginTop: 5}} style={{ marginTop: 5 }}
value={userState?.user?.aff_quota} value={userState?.user?.aff_quota}
disabled={true} disabled={true}
></Input> ></Input>
</div> </div>
<div style={{marginTop: 20}}> <div style={{ marginTop: 20 }}>
<Typography.Text> <Typography.Text>
{t('划转额度')}{renderQuotaWithPrompt(transferAmount)} {t('最低') + renderQuota(getQuotaPerUnit())} {t('划转额度')}{renderQuotaWithPrompt(transferAmount)} {t('最低') + renderQuota(getQuotaPerUnit())}
</Typography.Text> </Typography.Text>
<div> <div>
<InputNumber <InputNumber
min={0} min={0}
style={{marginTop: 5}} style={{ marginTop: 5 }}
value={transferAmount} value={transferAmount}
onChange={(value) => setTransferAmount(value)} onChange={(value) => setTransferAmount(value)}
disabled={false} disabled={false}
@@ -399,9 +405,9 @@ const PersonalSetting = () => {
<Card.Meta <Card.Meta
avatar={ avatar={
<Avatar <Avatar
size='default' size="default"
color={stringToColor(getUsername())} color={stringToColor(getUsername())}
style={{marginRight: 4}} style={{ marginRight: 4 }}
> >
{typeof getUsername() === 'string' && {typeof getUsername() === 'string' &&
getUsername().slice(0, 1)} getUsername().slice(0, 1)}
@@ -410,18 +416,18 @@ const PersonalSetting = () => {
title={<Typography.Text>{getUsername()}</Typography.Text>} title={<Typography.Text>{getUsername()}</Typography.Text>}
description={ description={
isRoot() ? ( isRoot() ? (
<Tag color='red'>{t('管理员')}</Tag> <Tag color="red">{t('管理员')}</Tag>
) : ( ) : (
<Tag color='blue'>{t('普通用户')}</Tag> <Tag color="blue">{t('普通用户')}</Tag>
) )
} }
></Card.Meta> ></Card.Meta>
} }
headerExtraContent={ headerExtraContent={
<> <>
<Space vertical align='start'> <Space vertical align="start">
<Tag color='green'>{'ID: ' + userState?.user?.id}</Tag> <Tag color="green">{'ID: ' + userState?.user?.id}</Tag>
<Tag color='blue'>{userState?.user?.group}</Tag> <Tag color="blue">{userState?.user?.group}</Tag>
</Space> </Space>
</> </>
} }
@@ -430,13 +436,13 @@ const PersonalSetting = () => {
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Typography.Title heading={6}>{t('可用模型')}</Typography.Title> <Typography.Title heading={6}>{t('可用模型')}</Typography.Title>
</div> </div>
<div style={{marginTop: 10}}> <div style={{ marginTop: 10 }}>
{models.length <= MODELS_DISPLAY_COUNT ? ( {models.length <= MODELS_DISPLAY_COUNT ? (
<Space wrap> <Space wrap>
{models.map((model) => ( {models.map((model) => (
<Tag <Tag
key={model} key={model}
color='cyan' color="cyan"
onClick={() => { onClick={() => {
copyText(model); copyText(model);
}} }}
@@ -452,7 +458,7 @@ const PersonalSetting = () => {
{models.map((model) => ( {models.map((model) => (
<Tag <Tag
key={model} key={model}
color='cyan' color="cyan"
onClick={() => { onClick={() => {
copyText(model); copyText(model);
}} }}
@@ -461,7 +467,7 @@ const PersonalSetting = () => {
</Tag> </Tag>
))} ))}
<Tag <Tag
color='blue' color="blue"
type="light" type="light"
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => setIsModelsExpanded(false)} onClick={() => setIsModelsExpanded(false)}
@@ -475,7 +481,7 @@ const PersonalSetting = () => {
{models.slice(0, MODELS_DISPLAY_COUNT).map((model) => ( {models.slice(0, MODELS_DISPLAY_COUNT).map((model) => (
<Tag <Tag
key={model} key={model}
color='cyan' color="cyan"
onClick={() => { onClick={() => {
copyText(model); copyText(model);
}} }}
@@ -484,7 +490,7 @@ const PersonalSetting = () => {
</Tag> </Tag>
))} ))}
<Tag <Tag
color='blue' color="blue"
type="light" type="light"
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => setIsModelsExpanded(true)} onClick={() => setIsModelsExpanded(true)}
@@ -513,12 +519,12 @@ const PersonalSetting = () => {
</Descriptions> </Descriptions>
</Card> </Card>
<Card <Card
style={{marginTop: 10}} style={{ marginTop: 10 }}
footer={ footer={
<div> <div>
<Typography.Text>{t('邀请链接')}</Typography.Text> <Typography.Text>{t('邀请链接')}</Typography.Text>
<Input <Input
style={{marginTop: 10}} style={{ marginTop: 10 }}
value={affLink} value={affLink}
onClick={handleAffLinkClick} onClick={handleAffLinkClick}
readOnly readOnly
@@ -527,17 +533,17 @@ const PersonalSetting = () => {
} }
> >
<Typography.Title heading={6}>{t('邀请信息')}</Typography.Title> <Typography.Title heading={6}>{t('邀请信息')}</Typography.Title>
<div style={{marginTop: 10}}> <div style={{ marginTop: 10 }}>
<Descriptions row> <Descriptions row>
<Descriptions.Item itemKey={t('待使用收益')}> <Descriptions.Item itemKey={t('待使用收益')}>
<span style={{color: 'rgba(var(--semi-red-5), 1)'}}> <span style={{ color: 'rgba(var(--semi-red-5), 1)' }}>
{renderQuota(userState?.user?.aff_quota)} {renderQuota(userState?.user?.aff_quota)}
</span> </span>
<Button <Button
type={'secondary'} type={'secondary'}
onClick={() => setOpenTransfer(true)} onClick={() => setOpenTransfer(true)}
size={'small'} size={'small'}
style={{marginLeft: 10}} style={{ marginLeft: 10 }}
> >
{t('划转')} {t('划转')}
</Button> </Button>
@@ -551,12 +557,12 @@ const PersonalSetting = () => {
</Descriptions> </Descriptions>
</div> </div>
</Card> </Card>
<Card style={{marginTop: 10}}> <Card style={{ marginTop: 10 }}>
<Typography.Title heading={6}>{t('个人信息')}</Typography.Title> <Typography.Title heading={6}>{t('个人信息')}</Typography.Title>
<div style={{marginTop: 20}}> <div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('邮箱')}</Typography.Text> <Typography.Text strong>{t('邮箱')}</Typography.Text>
<div <div
style={{display: 'flex', justifyContent: 'space-between'}} style={{ display: 'flex', justifyContent: 'space-between' }}
> >
<div> <div>
<Input <Input
@@ -581,9 +587,9 @@ const PersonalSetting = () => {
</div> </div>
</div> </div>
</div> </div>
<div style={{marginTop: 10}}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('微信')}</Typography.Text> <Typography.Text strong>{t('微信')}</Typography.Text>
<div style={{display: 'flex', justifyContent: 'space-between'}}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div> <div>
<Input <Input
value={ value={
@@ -610,10 +616,10 @@ const PersonalSetting = () => {
</div> </div>
</div> </div>
</div> </div>
<div style={{marginTop: 10}}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('GitHub')}</Typography.Text> <Typography.Text strong>{t('GitHub')}</Typography.Text>
<div <div
style={{display: 'flex', justifyContent: 'space-between'}} style={{ display: 'flex', justifyContent: 'space-between' }}
> >
<div> <div>
<Input <Input
@@ -640,10 +646,10 @@ const PersonalSetting = () => {
</div> </div>
</div> </div>
</div> </div>
<div style={{marginTop: 10}}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('OIDC')}</Typography.Text> <Typography.Text strong>{t('OIDC')}</Typography.Text>
<div <div
style={{display: 'flex', justifyContent: 'space-between'}} style={{ display: 'flex', justifyContent: 'space-between' }}
> >
<div> <div>
<Input <Input
@@ -670,10 +676,10 @@ const PersonalSetting = () => {
</div> </div>
</div> </div>
</div> </div>
<div style={{marginTop: 10}}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('Telegram')}</Typography.Text> <Typography.Text strong>{t('Telegram')}</Typography.Text>
<div <div
style={{display: 'flex', justifyContent: 'space-between'}} style={{ display: 'flex', justifyContent: 'space-between' }}
> >
<div> <div>
<Input <Input
@@ -691,7 +697,7 @@ const PersonalSetting = () => {
<Button disabled={true}>{t('已绑定')}</Button> <Button disabled={true}>{t('已绑定')}</Button>
) : ( ) : (
<TelegramLoginButton <TelegramLoginButton
dataAuthUrl='/api/oauth/telegram/bind' dataAuthUrl="/api/oauth/telegram/bind"
botName={status.telegram_bot_name} botName={status.telegram_bot_name}
/> />
) )
@@ -701,10 +707,10 @@ const PersonalSetting = () => {
</div> </div>
</div> </div>
</div> </div>
<div style={{marginTop: 10}}> <div style={{ marginTop: 10 }}>
<Typography.Text strong>{t('LinuxDO')}</Typography.Text> <Typography.Text strong>{t('LinuxDO')}</Typography.Text>
<div <div
style={{display: 'flex', justifyContent: 'space-between'}} style={{ display: 'flex', justifyContent: 'space-between' }}
> >
<div> <div>
<Input <Input
@@ -731,7 +737,7 @@ const PersonalSetting = () => {
</div> </div>
</div> </div>
</div> </div>
<div style={{marginTop: 10}}> <div style={{ marginTop: 10 }}>
<Space> <Space>
<Button onClick={generateAccessToken}> <Button onClick={generateAccessToken}>
{t('生成系统访问令牌')} {t('生成系统访问令牌')}
@@ -758,7 +764,7 @@ const PersonalSetting = () => {
readOnly readOnly
value={systemToken} value={systemToken}
onClick={handleSystemTokenClick} onClick={handleSystemTokenClick}
style={{marginTop: '10px'}} style={{ marginTop: '10px' }}
/> />
)} )}
<Modal <Modal
@@ -766,31 +772,48 @@ const PersonalSetting = () => {
visible={showWeChatBindModal} visible={showWeChatBindModal}
size={'small'} size={'small'}
> >
<Image src={status.wechat_qrcode}/> <Image src={status.wechat_qrcode} />
<div style={{textAlign: 'center'}}> <div style={{ textAlign: 'center' }}>
<p> <p>
微信扫码关注公众号输入验证码获取验证码三分钟内有效 微信扫码关注公众号输入验证码获取验证码三分钟内有效
</p> </p>
</div> </div>
<Input <Input
placeholder='验证码' placeholder="验证码"
name='wechat_verification_code' name="wechat_verification_code"
value={inputs.wechat_verification_code} value={inputs.wechat_verification_code}
onChange={(v) => onChange={(v) =>
handleInputChange('wechat_verification_code', v) handleInputChange('wechat_verification_code', v)
} }
/> />
<Button color='' fluid size='large' onClick={bindWeChat}> <Button color="" fluid size="large" onClick={bindWeChat}>
{t('绑定')} {t('绑定')}
</Button> </Button>
</Modal> </Modal>
</div> </div>
</Card> </Card>
<Card style={{marginTop: 10}}> <Card style={{ marginTop: 10 }}>
<Typography.Title heading={6}>{t('通知设置')}</Typography.Title> <Tabs type="line" defaultActiveKey="price">
<div style={{marginTop: 20}}> <TabPane tab={t('价格设置')} itemKey="price">
<div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('接受未设置价格模型')}</Typography.Text>
<div style={{ marginTop: 10 }}>
<Checkbox
checked={notificationSettings.acceptUnsetModelRatioModel}
onChange={e => handleNotificationSettingChange('acceptUnsetModelRatioModel', e.target.checked)}
>
{t('接受未设置价格模型')}
</Checkbox>
<Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
{t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
</Typography.Text>
</div>
</div>
</TabPane>
<TabPane tab={t('通知设置')} itemKey="notification">
<div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('通知方式')}</Typography.Text> <Typography.Text strong>{t('通知方式')}</Typography.Text>
<div style={{marginTop: 10}}> <div style={{ marginTop: 10 }}>
<RadioGroup <RadioGroup
value={notificationSettings.warningType} value={notificationSettings.warningType}
onChange={value => handleNotificationSettingChange('warningType', value)} onChange={value => handleNotificationSettingChange('warningType', value)}
@@ -802,23 +825,28 @@ const PersonalSetting = () => {
</div> </div>
{notificationSettings.warningType === 'webhook' && ( {notificationSettings.warningType === 'webhook' && (
<> <>
<div style={{marginTop: 20}}> <div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('Webhook地址')}</Typography.Text> <Typography.Text strong>{t('Webhook地址')}</Typography.Text>
<div style={{marginTop: 10}}> <div style={{ marginTop: 10 }}>
<Input <Input
value={notificationSettings.webhookUrl} value={notificationSettings.webhookUrl}
onChange={val => handleNotificationSettingChange('webhookUrl', val)} onChange={val => handleNotificationSettingChange('webhookUrl', val)}
placeholder={t('请输入Webhook地址例如: https://example.com/webhook')} placeholder={t('请输入Webhook地址例如: https://example.com/webhook')}
/> />
<Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}> <Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
{t('只支持https系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')} {t('只支持https系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')}
</Typography.Text> </Typography.Text>
<Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}> <Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
<div style={{cursor: 'pointer'}} onClick={() => setShowWebhookDocs(!showWebhookDocs)}> <div style={{ cursor: 'pointer' }} onClick={() => setShowWebhookDocs(!showWebhookDocs)}>
{t('Webhook请求结构')} {showWebhookDocs ? '▼' : '▶'} {t('Webhook请求结构')} {showWebhookDocs ? '▼' : '▶'}
</div> </div>
<Collapsible isOpen={showWebhookDocs}> <Collapsible isOpen={showWebhookDocs}>
<pre style={{marginTop: 4, background: 'var(--semi-color-fill-0)', padding: 8, borderRadius: 4}}> <pre style={{
marginTop: 4,
background: 'var(--semi-color-fill-0)',
padding: 8,
borderRadius: 4
}}>
{`{ {`{
"type": "quota_exceed", // 通知类型 "type": "quota_exceed", // 通知类型
"title": "标题", // 通知标题 "title": "标题", // 通知标题
@@ -840,18 +868,18 @@ const PersonalSetting = () => {
</Typography.Text> </Typography.Text>
</div> </div>
</div> </div>
<div style={{marginTop: 20}}> <div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('接口凭证(可选)')}</Typography.Text> <Typography.Text strong>{t('接口凭证(可选)')}</Typography.Text>
<div style={{marginTop: 10}}> <div style={{ marginTop: 10 }}>
<Input <Input
value={notificationSettings.webhookSecret} value={notificationSettings.webhookSecret}
onChange={val => handleNotificationSettingChange('webhookSecret', val)} onChange={val => handleNotificationSettingChange('webhookSecret', val)}
placeholder={t('请输入密钥')} placeholder={t('请输入密钥')}
/> />
<Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}> <Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
{t('密钥将以 Bearer 方式添加到请求头中用于验证webhook请求的合法性')} {t('密钥将以 Bearer 方式添加到请求头中用于验证webhook请求的合法性')}
</Typography.Text> </Typography.Text>
<Typography.Text type="secondary" style={{marginTop: 4, display: 'block'}}> <Typography.Text type="secondary" style={{ marginTop: 4, display: 'block' }}>
{t('Authorization: Bearer your-secret-key')} {t('Authorization: Bearer your-secret-key')}
</Typography.Text> </Typography.Text>
</div> </div>
@@ -859,27 +887,28 @@ const PersonalSetting = () => {
</> </>
)} )}
{notificationSettings.warningType === 'email' && ( {notificationSettings.warningType === 'email' && (
<div style={{marginTop: 20}}> <div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('通知邮箱')}</Typography.Text> <Typography.Text strong>{t('通知邮箱')}</Typography.Text>
<div style={{marginTop: 10}}> <div style={{ marginTop: 10 }}>
<Input <Input
value={notificationSettings.notificationEmail} value={notificationSettings.notificationEmail}
onChange={val => handleNotificationSettingChange('notificationEmail', val)} onChange={val => handleNotificationSettingChange('notificationEmail', val)}
placeholder={t('留空则使用账号绑定的邮箱')} placeholder={t('留空则使用账号绑定的邮箱')}
/> />
<Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}> <Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
{t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')} {t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
</Typography.Text> </Typography.Text>
</div> </div>
</div> </div>
)} )}
<div style={{marginTop: 20}}> <div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}</Typography.Text> <Typography.Text
<div style={{marginTop: 10}}> strong>{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}</Typography.Text>
<div style={{ marginTop: 10 }}>
<AutoComplete <AutoComplete
value={notificationSettings.warningThreshold} value={notificationSettings.warningThreshold}
onChange={val => handleNotificationSettingChange('warningThreshold', val)} onChange={val => handleNotificationSettingChange('warningThreshold', val)}
style={{width: 200}} style={{ width: 200 }}
placeholder={t('请输入预警额度')} placeholder={t('请输入预警额度')}
data={[ data={[
{ value: 100000, label: '0.2$' }, { value: 100000, label: '0.2$' },
@@ -889,11 +918,13 @@ const PersonalSetting = () => {
]} ]}
/> />
</div> </div>
<Typography.Text type="secondary" style={{marginTop: 10, display: 'block'}}> <Typography.Text type="secondary" style={{ marginTop: 10, display: 'block' }}>
{t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')} {t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
</Typography.Text> </Typography.Text>
</div> </div>
<div style={{marginTop: 20}}> </TabPane>
</Tabs>
<div style={{ marginTop: 20 }}>
<Button type="primary" onClick={saveNotificationSettings}> <Button type="primary" onClick={saveNotificationSettings}>
{t('保存设置')} {t('保存设置')}
</Button> </Button>
@@ -912,15 +943,15 @@ const PersonalSetting = () => {
style={{ style={{
marginTop: 20, marginTop: 20,
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between'
}} }}
> >
<Input <Input
fluid fluid
placeholder='输入邮箱地址' placeholder="输入邮箱地址"
onChange={(value) => handleInputChange('email', value)} onChange={(value) => handleInputChange('email', value)}
name='email' name="email"
type='email' type="email"
/> />
<Button <Button
onClick={sendVerificationCode} onClick={sendVerificationCode}
@@ -929,11 +960,11 @@ const PersonalSetting = () => {
{disableButton ? `重新发送 (${countdown})` : '获取验证码'} {disableButton ? `重新发送 (${countdown})` : '获取验证码'}
</Button> </Button>
</div> </div>
<div style={{marginTop: 10}}> <div style={{ marginTop: 10 }}>
<Input <Input
fluid fluid
placeholder='验证码' placeholder="验证码"
name='email_verification_code' name="email_verification_code"
value={inputs.email_verification_code} value={inputs.email_verification_code}
onChange={(value) => onChange={(value) =>
handleInputChange('email_verification_code', value) handleInputChange('email_verification_code', value)
@@ -958,22 +989,22 @@ const PersonalSetting = () => {
centered={true} centered={true}
onOk={deleteAccount} onOk={deleteAccount}
> >
<div style={{marginTop: 20}}> <div style={{ marginTop: 20 }}>
<Banner <Banner
type='danger' type="danger"
description='您正在删除自己的帐户,将清空所有数据且不可恢复' description="您正在删除自己的帐户,将清空所有数据且不可恢复"
closeIcon={null} closeIcon={null}
/> />
</div> </div>
<div style={{marginTop: 20}}> <div style={{ marginTop: 20 }}>
<Input <Input
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`} placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
name='self_account_deletion_confirmation' name="self_account_deletion_confirmation"
value={inputs.self_account_deletion_confirmation} value={inputs.self_account_deletion_confirmation}
onChange={(value) => onChange={(value) =>
handleInputChange( handleInputChange(
'self_account_deletion_confirmation', 'self_account_deletion_confirmation',
value, value
) )
} }
/> />
@@ -996,9 +1027,9 @@ const PersonalSetting = () => {
centered={true} centered={true}
onOk={changePassword} onOk={changePassword}
> >
<div style={{marginTop: 20}}> <div style={{ marginTop: 20 }}>
<Input <Input
name='set_new_password' name="set_new_password"
placeholder={t('新密码')} placeholder={t('新密码')}
value={inputs.set_new_password} value={inputs.set_new_password}
onChange={(value) => onChange={(value) =>
@@ -1006,8 +1037,8 @@ const PersonalSetting = () => {
} }
/> />
<Input <Input
style={{marginTop: 20}} style={{ marginTop: 20 }}
name='set_new_password_confirmation' name="set_new_password_confirmation"
placeholder={t('确认新密码')} placeholder={t('确认新密码')}
value={inputs.set_new_password_confirmation} value={inputs.set_new_password_confirmation}
onChange={(value) => onChange={(value) =>