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)

5185
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ import {
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';
@@ -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]);
@@ -217,7 +222,7 @@ 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) {
@@ -234,7 +239,7 @@ 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) {
@@ -252,7 +257,7 @@ 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) {
@@ -276,7 +281,7 @@ 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) {
@@ -294,7 +299,7 @@ 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) {
@@ -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) {
@@ -399,7 +405,7 @@ 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 }}
> >
@@ -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>
</> </>
} }
@@ -436,7 +442,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);
}} }}
@@ -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)}
@@ -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}
/> />
) )
@@ -773,21 +779,38 @@ const PersonalSetting = () => {
</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">
<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 }}> <div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('通知方式')}</Typography.Text> <Typography.Text strong>{t('通知方式')}</Typography.Text>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
@@ -818,7 +841,12 @@ const PersonalSetting = () => {
{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": "标题", // 通知标题
@@ -874,7 +902,8 @@ const PersonalSetting = () => {
</div> </div>
)} )}
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Typography.Text strong>{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}</Typography.Text> <Typography.Text
strong>{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}</Typography.Text>
<div style={{ marginTop: 10 }}> <div style={{ marginTop: 10 }}>
<AutoComplete <AutoComplete
value={notificationSettings.warningThreshold} value={notificationSettings.warningThreshold}
@@ -893,6 +922,8 @@ const PersonalSetting = () => {
{t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')} {t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
</Typography.Text> </Typography.Text>
</div> </div>
</TabPane>
</Tabs>
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<Button type="primary" onClick={saveNotificationSettings}> <Button type="primary" onClick={saveNotificationSettings}>
{t('保存设置')} {t('保存设置')}
@@ -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}
@@ -932,8 +963,8 @@ const PersonalSetting = () => {
<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)
@@ -960,20 +991,20 @@ const PersonalSetting = () => {
> >
<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
) )
} }
/> />
@@ -998,7 +1029,7 @@ const PersonalSetting = () => {
> >
<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) =>
@@ -1007,7 +1038,7 @@ 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) =>