feat: 优化代码,去除多余注释和修改
This commit is contained in:
402
.old/option.go
Normal file
402
.old/option.go
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/setting"
|
||||||
|
"one-api/setting/config"
|
||||||
|
"one-api/setting/operation_setting"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
Key string `json:"key" gorm:"primaryKey"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllOption() ([]*Option, error) {
|
||||||
|
var options []*Option
|
||||||
|
var err error
|
||||||
|
err = DB.Find(&options).Error
|
||||||
|
return options, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitOptionMap() {
|
||||||
|
common.OptionMapRWMutex.Lock()
|
||||||
|
common.OptionMap = make(map[string]string)
|
||||||
|
|
||||||
|
// 添加原有的系统配置
|
||||||
|
common.OptionMap["FileUploadPermission"] = strconv.Itoa(common.FileUploadPermission)
|
||||||
|
common.OptionMap["FileDownloadPermission"] = strconv.Itoa(common.FileDownloadPermission)
|
||||||
|
common.OptionMap["ImageUploadPermission"] = strconv.Itoa(common.ImageUploadPermission)
|
||||||
|
common.OptionMap["ImageDownloadPermission"] = strconv.Itoa(common.ImageDownloadPermission)
|
||||||
|
common.OptionMap["PasswordLoginEnabled"] = strconv.FormatBool(common.PasswordLoginEnabled)
|
||||||
|
common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled)
|
||||||
|
common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled)
|
||||||
|
common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled)
|
||||||
|
common.OptionMap["LinuxDOOAuthEnabled"] = strconv.FormatBool(common.LinuxDOOAuthEnabled)
|
||||||
|
common.OptionMap["TelegramOAuthEnabled"] = strconv.FormatBool(common.TelegramOAuthEnabled)
|
||||||
|
common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
|
||||||
|
common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled)
|
||||||
|
common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
|
||||||
|
common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
|
||||||
|
common.OptionMap["AutomaticEnableChannelEnabled"] = strconv.FormatBool(common.AutomaticEnableChannelEnabled)
|
||||||
|
common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled)
|
||||||
|
common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
|
||||||
|
common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled)
|
||||||
|
common.OptionMap["DrawingEnabled"] = strconv.FormatBool(common.DrawingEnabled)
|
||||||
|
common.OptionMap["TaskEnabled"] = strconv.FormatBool(common.TaskEnabled)
|
||||||
|
common.OptionMap["DataExportEnabled"] = strconv.FormatBool(common.DataExportEnabled)
|
||||||
|
common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64)
|
||||||
|
common.OptionMap["EmailDomainRestrictionEnabled"] = strconv.FormatBool(common.EmailDomainRestrictionEnabled)
|
||||||
|
common.OptionMap["EmailAliasRestrictionEnabled"] = strconv.FormatBool(common.EmailAliasRestrictionEnabled)
|
||||||
|
common.OptionMap["EmailDomainWhitelist"] = strings.Join(common.EmailDomainWhitelist, ",")
|
||||||
|
common.OptionMap["SMTPServer"] = ""
|
||||||
|
common.OptionMap["SMTPFrom"] = ""
|
||||||
|
common.OptionMap["SMTPPort"] = strconv.Itoa(common.SMTPPort)
|
||||||
|
common.OptionMap["SMTPAccount"] = ""
|
||||||
|
common.OptionMap["SMTPToken"] = ""
|
||||||
|
common.OptionMap["SMTPSSLEnabled"] = strconv.FormatBool(common.SMTPSSLEnabled)
|
||||||
|
common.OptionMap["Notice"] = ""
|
||||||
|
common.OptionMap["About"] = ""
|
||||||
|
common.OptionMap["HomePageContent"] = ""
|
||||||
|
common.OptionMap["Footer"] = common.Footer
|
||||||
|
common.OptionMap["SystemName"] = common.SystemName
|
||||||
|
common.OptionMap["Logo"] = common.Logo
|
||||||
|
common.OptionMap["ServerAddress"] = ""
|
||||||
|
common.OptionMap["WorkerUrl"] = setting.WorkerUrl
|
||||||
|
common.OptionMap["WorkerValidKey"] = setting.WorkerValidKey
|
||||||
|
common.OptionMap["PayAddress"] = ""
|
||||||
|
common.OptionMap["CustomCallbackAddress"] = ""
|
||||||
|
common.OptionMap["EpayId"] = ""
|
||||||
|
common.OptionMap["EpayKey"] = ""
|
||||||
|
common.OptionMap["Price"] = strconv.FormatFloat(setting.Price, 'f', -1, 64)
|
||||||
|
common.OptionMap["MinTopUp"] = strconv.Itoa(setting.MinTopUp)
|
||||||
|
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
|
||||||
|
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
||||||
|
common.OptionMap["GitHubClientId"] = ""
|
||||||
|
common.OptionMap["GitHubClientSecret"] = ""
|
||||||
|
common.OptionMap["TelegramBotToken"] = ""
|
||||||
|
common.OptionMap["TelegramBotName"] = ""
|
||||||
|
common.OptionMap["WeChatServerAddress"] = ""
|
||||||
|
common.OptionMap["WeChatServerToken"] = ""
|
||||||
|
common.OptionMap["WeChatAccountQRCodeImageURL"] = ""
|
||||||
|
common.OptionMap["TurnstileSiteKey"] = ""
|
||||||
|
common.OptionMap["TurnstileSecretKey"] = ""
|
||||||
|
common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser)
|
||||||
|
common.OptionMap["QuotaForInviter"] = strconv.Itoa(common.QuotaForInviter)
|
||||||
|
common.OptionMap["QuotaForInvitee"] = strconv.Itoa(common.QuotaForInvitee)
|
||||||
|
common.OptionMap["QuotaRemindThreshold"] = strconv.Itoa(common.QuotaRemindThreshold)
|
||||||
|
common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota)
|
||||||
|
common.OptionMap["ModelRequestRateLimitCount"] = strconv.Itoa(setting.ModelRequestRateLimitCount)
|
||||||
|
common.OptionMap["ModelRequestRateLimitDurationMinutes"] = strconv.Itoa(setting.ModelRequestRateLimitDurationMinutes)
|
||||||
|
common.OptionMap["ModelRequestRateLimitSuccessCount"] = strconv.Itoa(setting.ModelRequestRateLimitSuccessCount)
|
||||||
|
common.OptionMap["ModelRatio"] = operation_setting.ModelRatio2JSONString()
|
||||||
|
common.OptionMap["ModelPrice"] = operation_setting.ModelPrice2JSONString()
|
||||||
|
common.OptionMap["CacheRatio"] = operation_setting.CacheRatio2JSONString()
|
||||||
|
common.OptionMap["GroupRatio"] = setting.GroupRatio2JSONString()
|
||||||
|
common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString()
|
||||||
|
common.OptionMap["CompletionRatio"] = operation_setting.CompletionRatio2JSONString()
|
||||||
|
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||||
|
//common.OptionMap["ChatLink"] = common.ChatLink
|
||||||
|
//common.OptionMap["ChatLink2"] = common.ChatLink2
|
||||||
|
common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64)
|
||||||
|
common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes)
|
||||||
|
common.OptionMap["DataExportInterval"] = strconv.Itoa(common.DataExportInterval)
|
||||||
|
common.OptionMap["DataExportDefaultTime"] = common.DataExportDefaultTime
|
||||||
|
common.OptionMap["DefaultCollapseSidebar"] = strconv.FormatBool(common.DefaultCollapseSidebar)
|
||||||
|
common.OptionMap["MjNotifyEnabled"] = strconv.FormatBool(setting.MjNotifyEnabled)
|
||||||
|
common.OptionMap["MjAccountFilterEnabled"] = strconv.FormatBool(setting.MjAccountFilterEnabled)
|
||||||
|
common.OptionMap["MjModeClearEnabled"] = strconv.FormatBool(setting.MjModeClearEnabled)
|
||||||
|
common.OptionMap["MjForwardUrlEnabled"] = strconv.FormatBool(setting.MjForwardUrlEnabled)
|
||||||
|
common.OptionMap["MjActionCheckSuccessEnabled"] = strconv.FormatBool(setting.MjActionCheckSuccessEnabled)
|
||||||
|
common.OptionMap["CheckSensitiveEnabled"] = strconv.FormatBool(setting.CheckSensitiveEnabled)
|
||||||
|
common.OptionMap["DemoSiteEnabled"] = strconv.FormatBool(operation_setting.DemoSiteEnabled)
|
||||||
|
common.OptionMap["SelfUseModeEnabled"] = strconv.FormatBool(operation_setting.SelfUseModeEnabled)
|
||||||
|
common.OptionMap["ModelRequestRateLimitEnabled"] = strconv.FormatBool(setting.ModelRequestRateLimitEnabled)
|
||||||
|
common.OptionMap["CheckSensitiveOnPromptEnabled"] = strconv.FormatBool(setting.CheckSensitiveOnPromptEnabled)
|
||||||
|
common.OptionMap["StopOnSensitiveEnabled"] = strconv.FormatBool(setting.StopOnSensitiveEnabled)
|
||||||
|
common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
|
||||||
|
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
|
||||||
|
common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
|
||||||
|
|
||||||
|
// 自动添加所有注册的模型配置
|
||||||
|
modelConfigs := config.GlobalConfig.ExportAllConfigs()
|
||||||
|
for k, v := range modelConfigs {
|
||||||
|
common.OptionMap[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
common.OptionMapRWMutex.Unlock()
|
||||||
|
loadOptionsFromDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadOptionsFromDatabase() {
|
||||||
|
options, _ := AllOption()
|
||||||
|
for _, option := range options {
|
||||||
|
err := updateOptionMap(option.Key, option.Value)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("failed to update option map: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncOptions(frequency int) {
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Duration(frequency) * time.Second)
|
||||||
|
common.SysLog("syncing options from database")
|
||||||
|
loadOptionsFromDatabase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateOption(key string, value string) error {
|
||||||
|
// Save to database first
|
||||||
|
option := Option{
|
||||||
|
Key: key,
|
||||||
|
}
|
||||||
|
// https://gorm.io/docs/update.html#Save-All-Fields
|
||||||
|
DB.FirstOrCreate(&option, Option{Key: key})
|
||||||
|
option.Value = value
|
||||||
|
// Save is a combination function.
|
||||||
|
// If save value does not contain primary key, it will execute Create,
|
||||||
|
// otherwise it will execute Update (with all fields).
|
||||||
|
DB.Save(&option)
|
||||||
|
// Update OptionMap
|
||||||
|
return updateOptionMap(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateOptionMap(key string, value string) (err error) {
|
||||||
|
common.OptionMapRWMutex.Lock()
|
||||||
|
defer common.OptionMapRWMutex.Unlock()
|
||||||
|
common.OptionMap[key] = value
|
||||||
|
|
||||||
|
// 检查是否是模型配置 - 使用更规范的方式处理
|
||||||
|
if handleConfigUpdate(key, value) {
|
||||||
|
return nil // 已由配置系统处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理传统配置项...
|
||||||
|
if strings.HasSuffix(key, "Permission") {
|
||||||
|
intValue, _ := strconv.Atoi(value)
|
||||||
|
switch key {
|
||||||
|
case "FileUploadPermission":
|
||||||
|
common.FileUploadPermission = intValue
|
||||||
|
case "FileDownloadPermission":
|
||||||
|
common.FileDownloadPermission = intValue
|
||||||
|
case "ImageUploadPermission":
|
||||||
|
common.ImageUploadPermission = intValue
|
||||||
|
case "ImageDownloadPermission":
|
||||||
|
common.ImageDownloadPermission = intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(key, "Enabled") || key == "DefaultCollapseSidebar" {
|
||||||
|
boolValue := value == "true"
|
||||||
|
switch key {
|
||||||
|
case "PasswordRegisterEnabled":
|
||||||
|
common.PasswordRegisterEnabled = boolValue
|
||||||
|
case "PasswordLoginEnabled":
|
||||||
|
common.PasswordLoginEnabled = boolValue
|
||||||
|
case "EmailVerificationEnabled":
|
||||||
|
common.EmailVerificationEnabled = boolValue
|
||||||
|
case "GitHubOAuthEnabled":
|
||||||
|
common.GitHubOAuthEnabled = boolValue
|
||||||
|
case "LinuxDOOAuthEnabled":
|
||||||
|
common.LinuxDOOAuthEnabled = boolValue
|
||||||
|
case "WeChatAuthEnabled":
|
||||||
|
common.WeChatAuthEnabled = boolValue
|
||||||
|
case "TelegramOAuthEnabled":
|
||||||
|
common.TelegramOAuthEnabled = boolValue
|
||||||
|
case "TurnstileCheckEnabled":
|
||||||
|
common.TurnstileCheckEnabled = boolValue
|
||||||
|
case "RegisterEnabled":
|
||||||
|
common.RegisterEnabled = boolValue
|
||||||
|
case "EmailDomainRestrictionEnabled":
|
||||||
|
common.EmailDomainRestrictionEnabled = boolValue
|
||||||
|
case "EmailAliasRestrictionEnabled":
|
||||||
|
common.EmailAliasRestrictionEnabled = boolValue
|
||||||
|
case "AutomaticDisableChannelEnabled":
|
||||||
|
common.AutomaticDisableChannelEnabled = boolValue
|
||||||
|
case "AutomaticEnableChannelEnabled":
|
||||||
|
common.AutomaticEnableChannelEnabled = boolValue
|
||||||
|
case "LogConsumeEnabled":
|
||||||
|
common.LogConsumeEnabled = boolValue
|
||||||
|
case "DisplayInCurrencyEnabled":
|
||||||
|
common.DisplayInCurrencyEnabled = boolValue
|
||||||
|
case "DisplayTokenStatEnabled":
|
||||||
|
common.DisplayTokenStatEnabled = boolValue
|
||||||
|
case "DrawingEnabled":
|
||||||
|
common.DrawingEnabled = boolValue
|
||||||
|
case "TaskEnabled":
|
||||||
|
common.TaskEnabled = boolValue
|
||||||
|
case "DataExportEnabled":
|
||||||
|
common.DataExportEnabled = boolValue
|
||||||
|
case "DefaultCollapseSidebar":
|
||||||
|
common.DefaultCollapseSidebar = boolValue
|
||||||
|
case "MjNotifyEnabled":
|
||||||
|
setting.MjNotifyEnabled = boolValue
|
||||||
|
case "MjAccountFilterEnabled":
|
||||||
|
setting.MjAccountFilterEnabled = boolValue
|
||||||
|
case "MjModeClearEnabled":
|
||||||
|
setting.MjModeClearEnabled = boolValue
|
||||||
|
case "MjForwardUrlEnabled":
|
||||||
|
setting.MjForwardUrlEnabled = boolValue
|
||||||
|
case "MjActionCheckSuccessEnabled":
|
||||||
|
setting.MjActionCheckSuccessEnabled = boolValue
|
||||||
|
case "CheckSensitiveEnabled":
|
||||||
|
setting.CheckSensitiveEnabled = boolValue
|
||||||
|
case "DemoSiteEnabled":
|
||||||
|
operation_setting.DemoSiteEnabled = boolValue
|
||||||
|
case "SelfUseModeEnabled":
|
||||||
|
operation_setting.SelfUseModeEnabled = boolValue
|
||||||
|
case "CheckSensitiveOnPromptEnabled":
|
||||||
|
setting.CheckSensitiveOnPromptEnabled = boolValue
|
||||||
|
case "ModelRequestRateLimitEnabled":
|
||||||
|
setting.ModelRequestRateLimitEnabled = boolValue
|
||||||
|
case "StopOnSensitiveEnabled":
|
||||||
|
setting.StopOnSensitiveEnabled = boolValue
|
||||||
|
case "SMTPSSLEnabled":
|
||||||
|
common.SMTPSSLEnabled = boolValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "EmailDomainWhitelist":
|
||||||
|
common.EmailDomainWhitelist = strings.Split(value, ",")
|
||||||
|
case "SMTPServer":
|
||||||
|
common.SMTPServer = value
|
||||||
|
case "SMTPPort":
|
||||||
|
intValue, _ := strconv.Atoi(value)
|
||||||
|
common.SMTPPort = intValue
|
||||||
|
case "SMTPAccount":
|
||||||
|
common.SMTPAccount = value
|
||||||
|
case "SMTPFrom":
|
||||||
|
common.SMTPFrom = value
|
||||||
|
case "SMTPToken":
|
||||||
|
common.SMTPToken = value
|
||||||
|
case "ServerAddress":
|
||||||
|
setting.ServerAddress = value
|
||||||
|
case "WorkerUrl":
|
||||||
|
setting.WorkerUrl = value
|
||||||
|
case "WorkerValidKey":
|
||||||
|
setting.WorkerValidKey = value
|
||||||
|
case "PayAddress":
|
||||||
|
setting.PayAddress = value
|
||||||
|
case "Chats":
|
||||||
|
err = setting.UpdateChatsByJsonString(value)
|
||||||
|
case "CustomCallbackAddress":
|
||||||
|
setting.CustomCallbackAddress = value
|
||||||
|
case "EpayId":
|
||||||
|
setting.EpayId = value
|
||||||
|
case "EpayKey":
|
||||||
|
setting.EpayKey = value
|
||||||
|
case "Price":
|
||||||
|
setting.Price, _ = strconv.ParseFloat(value, 64)
|
||||||
|
case "MinTopUp":
|
||||||
|
setting.MinTopUp, _ = strconv.Atoi(value)
|
||||||
|
case "TopupGroupRatio":
|
||||||
|
err = common.UpdateTopupGroupRatioByJSONString(value)
|
||||||
|
case "GitHubClientId":
|
||||||
|
common.GitHubClientId = value
|
||||||
|
case "GitHubClientSecret":
|
||||||
|
common.GitHubClientSecret = value
|
||||||
|
case "LinuxDOClientId":
|
||||||
|
common.LinuxDOClientId = value
|
||||||
|
case "LinuxDOClientSecret":
|
||||||
|
common.LinuxDOClientSecret = value
|
||||||
|
case "Footer":
|
||||||
|
common.Footer = value
|
||||||
|
case "SystemName":
|
||||||
|
common.SystemName = value
|
||||||
|
case "Logo":
|
||||||
|
common.Logo = value
|
||||||
|
case "WeChatServerAddress":
|
||||||
|
common.WeChatServerAddress = value
|
||||||
|
case "WeChatServerToken":
|
||||||
|
common.WeChatServerToken = value
|
||||||
|
case "WeChatAccountQRCodeImageURL":
|
||||||
|
common.WeChatAccountQRCodeImageURL = value
|
||||||
|
case "TelegramBotToken":
|
||||||
|
common.TelegramBotToken = value
|
||||||
|
case "TelegramBotName":
|
||||||
|
common.TelegramBotName = value
|
||||||
|
case "TurnstileSiteKey":
|
||||||
|
common.TurnstileSiteKey = value
|
||||||
|
case "TurnstileSecretKey":
|
||||||
|
common.TurnstileSecretKey = value
|
||||||
|
case "QuotaForNewUser":
|
||||||
|
common.QuotaForNewUser, _ = strconv.Atoi(value)
|
||||||
|
case "QuotaForInviter":
|
||||||
|
common.QuotaForInviter, _ = strconv.Atoi(value)
|
||||||
|
case "QuotaForInvitee":
|
||||||
|
common.QuotaForInvitee, _ = strconv.Atoi(value)
|
||||||
|
case "QuotaRemindThreshold":
|
||||||
|
common.QuotaRemindThreshold, _ = strconv.Atoi(value)
|
||||||
|
case "PreConsumedQuota":
|
||||||
|
common.PreConsumedQuota, _ = strconv.Atoi(value)
|
||||||
|
case "ModelRequestRateLimitCount":
|
||||||
|
setting.ModelRequestRateLimitCount, _ = strconv.Atoi(value)
|
||||||
|
case "ModelRequestRateLimitDurationMinutes":
|
||||||
|
setting.ModelRequestRateLimitDurationMinutes, _ = strconv.Atoi(value)
|
||||||
|
case "ModelRequestRateLimitSuccessCount":
|
||||||
|
setting.ModelRequestRateLimitSuccessCount, _ = strconv.Atoi(value)
|
||||||
|
case "RetryTimes":
|
||||||
|
common.RetryTimes, _ = strconv.Atoi(value)
|
||||||
|
case "DataExportInterval":
|
||||||
|
common.DataExportInterval, _ = strconv.Atoi(value)
|
||||||
|
case "DataExportDefaultTime":
|
||||||
|
common.DataExportDefaultTime = value
|
||||||
|
case "ModelRatio":
|
||||||
|
err = operation_setting.UpdateModelRatioByJSONString(value)
|
||||||
|
case "GroupRatio":
|
||||||
|
err = setting.UpdateGroupRatioByJSONString(value)
|
||||||
|
case "UserUsableGroups":
|
||||||
|
err = setting.UpdateUserUsableGroupsByJSONString(value)
|
||||||
|
case "CompletionRatio":
|
||||||
|
err = operation_setting.UpdateCompletionRatioByJSONString(value)
|
||||||
|
case "ModelPrice":
|
||||||
|
err = operation_setting.UpdateModelPriceByJSONString(value)
|
||||||
|
case "CacheRatio":
|
||||||
|
err = operation_setting.UpdateCacheRatioByJSONString(value)
|
||||||
|
case "TopUpLink":
|
||||||
|
common.TopUpLink = value
|
||||||
|
//case "ChatLink":
|
||||||
|
// common.ChatLink = value
|
||||||
|
//case "ChatLink2":
|
||||||
|
// common.ChatLink2 = value
|
||||||
|
case "ChannelDisableThreshold":
|
||||||
|
common.ChannelDisableThreshold, _ = strconv.ParseFloat(value, 64)
|
||||||
|
case "QuotaPerUnit":
|
||||||
|
common.QuotaPerUnit, _ = strconv.ParseFloat(value, 64)
|
||||||
|
case "SensitiveWords":
|
||||||
|
setting.SensitiveWordsFromString(value)
|
||||||
|
case "AutomaticDisableKeywords":
|
||||||
|
operation_setting.AutomaticDisableKeywordsFromString(value)
|
||||||
|
case "StreamCacheQueueLength":
|
||||||
|
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleConfigUpdate 处理分层配置更新,返回是否已处理
|
||||||
|
func handleConfigUpdate(key, value string) bool {
|
||||||
|
parts := strings.SplitN(key, ".", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return false // 不是分层配置
|
||||||
|
}
|
||||||
|
|
||||||
|
configName := parts[0]
|
||||||
|
configKey := parts[1]
|
||||||
|
|
||||||
|
// 获取配置对象
|
||||||
|
cfg := config.GlobalConfig.Get(configName)
|
||||||
|
if cfg == nil {
|
||||||
|
return false // 未注册的配置
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
configMap := map[string]string{
|
||||||
|
configKey: value,
|
||||||
|
}
|
||||||
|
config.UpdateConfigFromMap(cfg, configMap)
|
||||||
|
|
||||||
|
return true // 已处理
|
||||||
|
}
|
||||||
@@ -19,25 +19,20 @@ const (
|
|||||||
ModelRequestRateLimitSuccessCountMark = "MRRLS"
|
ModelRequestRateLimitSuccessCountMark = "MRRLS"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 检查Redis中的请求限制
|
|
||||||
func checkRedisRateLimit(ctx context.Context, rdb *redis.Client, key string, maxCount int, duration int64) (bool, error) {
|
func checkRedisRateLimit(ctx context.Context, rdb *redis.Client, key string, maxCount int, duration int64) (bool, error) {
|
||||||
// 如果maxCount为0,表示不限制
|
|
||||||
if maxCount == 0 {
|
if maxCount == 0 {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前计数
|
|
||||||
length, err := rdb.LLen(ctx, key).Result()
|
length, err := rdb.LLen(ctx, key).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果未达到限制,允许请求
|
|
||||||
if length < int64(maxCount) {
|
if length < int64(maxCount) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查时间窗口
|
|
||||||
oldTimeStr, _ := rdb.LIndex(ctx, key, -1).Result()
|
oldTimeStr, _ := rdb.LIndex(ctx, key, -1).Result()
|
||||||
oldTime, err := time.Parse(timeFormat, oldTimeStr)
|
oldTime, err := time.Parse(timeFormat, oldTimeStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -49,7 +44,6 @@ func checkRedisRateLimit(ctx context.Context, rdb *redis.Client, key string, max
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
// 如果在时间窗口内已达到限制,拒绝请求
|
|
||||||
subTime := nowTime.Sub(oldTime).Seconds()
|
subTime := nowTime.Sub(oldTime).Seconds()
|
||||||
if int64(subTime) < duration {
|
if int64(subTime) < duration {
|
||||||
rdb.Expire(ctx, key, time.Duration(setting.ModelRequestRateLimitDurationMinutes)*time.Minute)
|
rdb.Expire(ctx, key, time.Duration(setting.ModelRequestRateLimitDurationMinutes)*time.Minute)
|
||||||
@@ -59,9 +53,7 @@ func checkRedisRateLimit(ctx context.Context, rdb *redis.Client, key string, max
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录Redis请求
|
|
||||||
func recordRedisRequest(ctx context.Context, rdb *redis.Client, key string, maxCount int) {
|
func recordRedisRequest(ctx context.Context, rdb *redis.Client, key string, maxCount int) {
|
||||||
// 如果maxCount为0,不记录请求
|
|
||||||
if maxCount == 0 {
|
if maxCount == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -72,14 +64,12 @@ func recordRedisRequest(ctx context.Context, rdb *redis.Client, key string, maxC
|
|||||||
rdb.Expire(ctx, key, time.Duration(setting.ModelRequestRateLimitDurationMinutes)*time.Minute)
|
rdb.Expire(ctx, key, time.Duration(setting.ModelRequestRateLimitDurationMinutes)*time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redis限流处理器
|
|
||||||
func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) gin.HandlerFunc {
|
func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
userId := strconv.Itoa(c.GetInt("id"))
|
userId := strconv.Itoa(c.GetInt("id"))
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
rdb := common.RDB
|
rdb := common.RDB
|
||||||
|
|
||||||
// 1. 检查成功请求数限制
|
|
||||||
successKey := fmt.Sprintf("rateLimit:%s:%s", ModelRequestRateLimitSuccessCountMark, userId)
|
successKey := fmt.Sprintf("rateLimit:%s:%s", ModelRequestRateLimitSuccessCountMark, userId)
|
||||||
allowed, err := checkRedisRateLimit(ctx, rdb, successKey, successMaxCount, duration)
|
allowed, err := checkRedisRateLimit(ctx, rdb, successKey, successMaxCount, duration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -92,9 +82,7 @@ func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) g
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//2.检查总请求数限制并记录总请求(当totalMaxCount为0时会自动跳过,使用令牌桶限流器
|
|
||||||
totalKey := fmt.Sprintf("rateLimit:%s", userId)
|
totalKey := fmt.Sprintf("rateLimit:%s", userId)
|
||||||
// 初始化
|
|
||||||
tb := limiter.New(ctx, rdb)
|
tb := limiter.New(ctx, rdb)
|
||||||
allowed, err = tb.Allow(
|
allowed, err = tb.Allow(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -114,17 +102,14 @@ func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) g
|
|||||||
abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到总请求数限制:%d分钟内最多请求%d次,包括失败次数,请检查您的请求是否正确", setting.ModelRequestRateLimitDurationMinutes, totalMaxCount))
|
abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到总请求数限制:%d分钟内最多请求%d次,包括失败次数,请检查您的请求是否正确", setting.ModelRequestRateLimitDurationMinutes, totalMaxCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 处理请求
|
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
||||||
// 5. 如果请求成功,记录成功请求
|
|
||||||
if c.Writer.Status() < 400 {
|
if c.Writer.Status() < 400 {
|
||||||
recordRedisRequest(ctx, rdb, successKey, successMaxCount)
|
recordRedisRequest(ctx, rdb, successKey, successMaxCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 内存限流处理器
|
|
||||||
func memoryRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) gin.HandlerFunc {
|
func memoryRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) gin.HandlerFunc {
|
||||||
inMemoryRateLimiter.Init(time.Duration(setting.ModelRequestRateLimitDurationMinutes) * time.Minute)
|
inMemoryRateLimiter.Init(time.Duration(setting.ModelRequestRateLimitDurationMinutes) * time.Minute)
|
||||||
|
|
||||||
@@ -133,15 +118,12 @@ func memoryRateLimitHandler(duration int64, totalMaxCount, successMaxCount int)
|
|||||||
totalKey := ModelRequestRateLimitCountMark + userId
|
totalKey := ModelRequestRateLimitCountMark + userId
|
||||||
successKey := ModelRequestRateLimitSuccessCountMark + userId
|
successKey := ModelRequestRateLimitSuccessCountMark + userId
|
||||||
|
|
||||||
// 1. 检查总请求数限制(当totalMaxCount为0时跳过)
|
|
||||||
if totalMaxCount > 0 && !inMemoryRateLimiter.Request(totalKey, totalMaxCount, duration) {
|
if totalMaxCount > 0 && !inMemoryRateLimiter.Request(totalKey, totalMaxCount, duration) {
|
||||||
c.Status(http.StatusTooManyRequests)
|
c.Status(http.StatusTooManyRequests)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 检查成功请求数限制
|
|
||||||
// 使用一个临时key来检查限制,这样可以避免实际记录
|
|
||||||
checkKey := successKey + "_check"
|
checkKey := successKey + "_check"
|
||||||
if !inMemoryRateLimiter.Request(checkKey, successMaxCount, duration) {
|
if !inMemoryRateLimiter.Request(checkKey, successMaxCount, duration) {
|
||||||
c.Status(http.StatusTooManyRequests)
|
c.Status(http.StatusTooManyRequests)
|
||||||
@@ -149,54 +131,47 @@ func memoryRateLimitHandler(duration int64, totalMaxCount, successMaxCount int)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 处理请求
|
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
||||||
// 4. 如果请求成功,记录到实际的成功请求计数中
|
|
||||||
if c.Writer.Status() < 400 {
|
if c.Writer.Status() < 400 {
|
||||||
inMemoryRateLimiter.Request(successKey, successMaxCount, duration)
|
inMemoryRateLimiter.Request(successKey, successMaxCount, duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModelRequestRateLimit 模型请求限流中间件
|
|
||||||
func ModelRequestRateLimit() func(c *gin.Context) {
|
func ModelRequestRateLimit() func(c *gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// 在每个请求时检查是否启用限流
|
|
||||||
if !setting.ModelRequestRateLimitEnabled {
|
if !setting.ModelRequestRateLimitEnabled {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算通用限流参数
|
|
||||||
duration := int64(setting.ModelRequestRateLimitDurationMinutes * 60)
|
duration := int64(setting.ModelRequestRateLimitDurationMinutes * 60)
|
||||||
|
|
||||||
// 获取用户组
|
|
||||||
group := c.GetString("token_group")
|
group := c.GetString("token_group")
|
||||||
if group == "" {
|
if group == "" {
|
||||||
group = c.GetString("group")
|
group = c.GetString("group")
|
||||||
}
|
}
|
||||||
if group == "" {
|
if group == "" {
|
||||||
group = "default" // 默认组
|
group = "default"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试获取用户组特定的限制
|
finalTotalCount := setting.ModelRequestRateLimitCount
|
||||||
|
finalSuccessCount := setting.ModelRequestRateLimitSuccessCount
|
||||||
|
foundGroupLimit := false
|
||||||
|
|
||||||
groupTotalCount, groupSuccessCount, found := setting.GetGroupRateLimit(group)
|
groupTotalCount, groupSuccessCount, found := setting.GetGroupRateLimit(group)
|
||||||
|
|
||||||
// 确定最终的限制值
|
|
||||||
finalTotalCount := setting.ModelRequestRateLimitCount // 默认使用全局总次数限制
|
|
||||||
finalSuccessCount := setting.ModelRequestRateLimitSuccessCount // 默认使用全局成功次数限制
|
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
// 如果找到用户组特定限制,则使用它们
|
|
||||||
finalTotalCount = groupTotalCount
|
finalTotalCount = groupTotalCount
|
||||||
finalSuccessCount = groupSuccessCount
|
finalSuccessCount = groupSuccessCount
|
||||||
|
foundGroupLimit = true
|
||||||
common.LogWarn(c.Request.Context(), fmt.Sprintf("Using rate limit for group '%s': total=%d, success=%d", group, finalTotalCount, finalSuccessCount))
|
common.LogWarn(c.Request.Context(), fmt.Sprintf("Using rate limit for group '%s': total=%d, success=%d", group, finalTotalCount, finalSuccessCount))
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if !foundGroupLimit {
|
||||||
common.LogInfo(c.Request.Context(), fmt.Sprintf("No specific rate limit found for group '%s', using global limits: total=%d, success=%d", group, finalTotalCount, finalSuccessCount))
|
common.LogInfo(c.Request.Context(), fmt.Sprintf("No specific rate limit found for group '%s', using global limits: total=%d, success=%d", group, finalTotalCount, finalSuccessCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据存储类型选择并执行限流处理器,传入最终确定的限制值
|
|
||||||
if common.RedisEnabled {
|
if common.RedisEnabled {
|
||||||
redisRateLimitHandler(duration, finalTotalCount, finalSuccessCount)(c)
|
redisRateLimitHandler(duration, finalTotalCount, finalSuccessCount)(c)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -94,11 +94,12 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["ModelRequestRateLimitCount"] = strconv.Itoa(setting.ModelRequestRateLimitCount)
|
common.OptionMap["ModelRequestRateLimitCount"] = strconv.Itoa(setting.ModelRequestRateLimitCount)
|
||||||
common.OptionMap["ModelRequestRateLimitDurationMinutes"] = strconv.Itoa(setting.ModelRequestRateLimitDurationMinutes)
|
common.OptionMap["ModelRequestRateLimitDurationMinutes"] = strconv.Itoa(setting.ModelRequestRateLimitDurationMinutes)
|
||||||
common.OptionMap["ModelRequestRateLimitSuccessCount"] = strconv.Itoa(setting.ModelRequestRateLimitSuccessCount)
|
common.OptionMap["ModelRequestRateLimitSuccessCount"] = strconv.Itoa(setting.ModelRequestRateLimitSuccessCount)
|
||||||
|
jsonBytes, _ := json.Marshal(map[string][2]int{})
|
||||||
|
common.OptionMap["ModelRequestRateLimitGroup"] = string(jsonBytes)
|
||||||
common.OptionMap["ModelRatio"] = operation_setting.ModelRatio2JSONString()
|
common.OptionMap["ModelRatio"] = operation_setting.ModelRatio2JSONString()
|
||||||
common.OptionMap["ModelPrice"] = operation_setting.ModelPrice2JSONString()
|
common.OptionMap["ModelPrice"] = operation_setting.ModelPrice2JSONString()
|
||||||
common.OptionMap["CacheRatio"] = operation_setting.CacheRatio2JSONString()
|
common.OptionMap["CacheRatio"] = operation_setting.CacheRatio2JSONString()
|
||||||
common.OptionMap["GroupRatio"] = setting.GroupRatio2JSONString()
|
common.OptionMap["GroupRatio"] = setting.GroupRatio2JSONString()
|
||||||
common.OptionMap[setting.ModelRequestRateLimitGroupKey] = "{}" // 添加用户组速率限制默认值
|
|
||||||
common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString()
|
common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString()
|
||||||
common.OptionMap["CompletionRatio"] = operation_setting.CompletionRatio2JSONString()
|
common.OptionMap["CompletionRatio"] = operation_setting.CompletionRatio2JSONString()
|
||||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||||
@@ -153,47 +154,31 @@ func SyncOptions(frequency int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateOption(key string, value string) error {
|
func UpdateOption(key string, value string) error {
|
||||||
originalValue := value // 保存原始值以备后用
|
originalValue := value
|
||||||
|
|
||||||
// Validate and format specific keys before saving
|
if key == "ModelRequestRateLimitGroup" {
|
||||||
if key == setting.ModelRequestRateLimitGroupKey {
|
|
||||||
var cfg map[string][2]int
|
var cfg map[string][2]int
|
||||||
// Validate the JSON structure first using the original value
|
|
||||||
err := json.Unmarshal([]byte(originalValue), &cfg)
|
err := json.Unmarshal([]byte(originalValue), &cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 提供更具体的错误信息
|
|
||||||
return fmt.Errorf("无效的 JSON 格式 for %s: %w", key, err)
|
return fmt.Errorf("无效的 JSON 格式 for %s: %w", key, err)
|
||||||
}
|
}
|
||||||
// TODO: 可以添加更细致的结构验证,例如检查数组长度是否为2,值是否为非负数等。
|
|
||||||
// if !isValidModelRequestRateLimitGroupConfig(cfg) {
|
|
||||||
// return fmt.Errorf("无效的配置值 for %s", key)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// If valid, format the JSON before saving
|
|
||||||
formattedValueBytes, marshalErr := json.MarshalIndent(cfg, "", " ")
|
formattedValueBytes, marshalErr := json.MarshalIndent(cfg, "", " ")
|
||||||
if marshalErr != nil {
|
if marshalErr != nil {
|
||||||
// This should ideally not happen if validation passed, but handle defensively
|
|
||||||
return fmt.Errorf("failed to marshal validated %s config: %w", key, marshalErr)
|
return fmt.Errorf("failed to marshal validated %s config: %w", key, marshalErr)
|
||||||
}
|
}
|
||||||
value = string(formattedValueBytes) // Use formatted JSON for saving and memory update
|
value = string(formattedValueBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to database
|
|
||||||
option := Option{
|
option := Option{
|
||||||
Key: key,
|
Key: key,
|
||||||
}
|
}
|
||||||
// https://gorm.io/docs/update.html#Save-All-Fields
|
|
||||||
DB.FirstOrCreate(&option, Option{Key: key})
|
DB.FirstOrCreate(&option, Option{Key: key})
|
||||||
option.Value = value
|
option.Value = value
|
||||||
// Save is a combination function.
|
|
||||||
// If save value does not contain primary key, it will execute Create,
|
|
||||||
// otherwise it will execute Update (with all fields).
|
|
||||||
if err := DB.Save(&option).Error; err != nil {
|
if err := DB.Save(&option).Error; err != nil {
|
||||||
return fmt.Errorf("保存选项 %s 到数据库失败: %w", key, err) // 添加错误上下文
|
return fmt.Errorf("保存选项 %s 到数据库失败: %w", key, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update OptionMap in memory using the potentially formatted value
|
|
||||||
// updateOptionMap 会处理内存中 setting.ModelRequestRateLimitGroupConfig 的更新
|
|
||||||
return updateOptionMap(key, value)
|
return updateOptionMap(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,6 +355,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
setting.ModelRequestRateLimitDurationMinutes, _ = strconv.Atoi(value)
|
setting.ModelRequestRateLimitDurationMinutes, _ = strconv.Atoi(value)
|
||||||
case "ModelRequestRateLimitSuccessCount":
|
case "ModelRequestRateLimitSuccessCount":
|
||||||
setting.ModelRequestRateLimitSuccessCount, _ = strconv.Atoi(value)
|
setting.ModelRequestRateLimitSuccessCount, _ = strconv.Atoi(value)
|
||||||
|
case "ModelRequestRateLimitGroup":
|
||||||
|
err = setting.UpdateModelRequestRateLimitGroup(value)
|
||||||
case "RetryTimes":
|
case "RetryTimes":
|
||||||
common.RetryTimes, _ = strconv.Atoi(value)
|
common.RetryTimes, _ = strconv.Atoi(value)
|
||||||
case "DataExportInterval":
|
case "DataExportInterval":
|
||||||
@@ -404,15 +391,6 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
operation_setting.AutomaticDisableKeywordsFromString(value)
|
operation_setting.AutomaticDisableKeywordsFromString(value)
|
||||||
case "StreamCacheQueueLength":
|
case "StreamCacheQueueLength":
|
||||||
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
|
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
|
||||||
case setting.ModelRequestRateLimitGroupKey:
|
|
||||||
// Use the (potentially formatted) value passed from UpdateOption
|
|
||||||
// to update the actual configuration in memory.
|
|
||||||
// This is the single point where the memory state for this specific setting is updated.
|
|
||||||
err = setting.UpdateModelRequestRateLimitGroupConfig(value)
|
|
||||||
if err != nil {
|
|
||||||
// 添加错误上下文
|
|
||||||
err = fmt.Errorf("更新内存中的 %s 配置失败: %w", key, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -440,4 +418,4 @@ func handleConfigUpdate(key, value string) bool {
|
|||||||
config.UpdateConfigFromMap(cfg, configMap)
|
config.UpdateConfigFromMap(cfg, configMap)
|
||||||
|
|
||||||
return true // 已处理
|
return true // 已处理
|
||||||
}
|
}
|
||||||
@@ -11,24 +11,17 @@ var ModelRequestRateLimitEnabled = false
|
|||||||
var ModelRequestRateLimitDurationMinutes = 1
|
var ModelRequestRateLimitDurationMinutes = 1
|
||||||
var ModelRequestRateLimitCount = 0
|
var ModelRequestRateLimitCount = 0
|
||||||
var ModelRequestRateLimitSuccessCount = 1000
|
var ModelRequestRateLimitSuccessCount = 1000
|
||||||
|
var ModelRequestRateLimitGroup map[string][2]int
|
||||||
|
|
||||||
// ModelRequestRateLimitGroupKey 定义了模型请求按组速率限制的配置键
|
|
||||||
const ModelRequestRateLimitGroupKey = "ModelRequestRateLimitGroup"
|
|
||||||
|
|
||||||
// ModelRequestRateLimitGroupConfig 存储按用户组解析后的速率限制配置
|
|
||||||
// map[groupName][2]int{totalCount, successCount}
|
|
||||||
var ModelRequestRateLimitGroupConfig map[string][2]int
|
|
||||||
var ModelRequestRateLimitGroupMutex sync.RWMutex
|
var ModelRequestRateLimitGroupMutex sync.RWMutex
|
||||||
|
|
||||||
// UpdateModelRequestRateLimitGroupConfig 解析、校验并更新内存中的用户组速率限制配置
|
func UpdateModelRequestRateLimitGroup(jsonStr string) error {
|
||||||
func UpdateModelRequestRateLimitGroupConfig(jsonStr string) error {
|
|
||||||
ModelRequestRateLimitGroupMutex.Lock()
|
ModelRequestRateLimitGroupMutex.Lock()
|
||||||
defer ModelRequestRateLimitGroupMutex.Unlock()
|
defer ModelRequestRateLimitGroupMutex.Unlock()
|
||||||
|
|
||||||
var newConfig map[string][2]int
|
var newConfig map[string][2]int
|
||||||
if jsonStr == "" || jsonStr == "{}" {
|
if jsonStr == "" || jsonStr == "{}" {
|
||||||
// 如果配置为空或空JSON对象,则清空内存配置
|
ModelRequestRateLimitGroup = make(map[string][2]int)
|
||||||
ModelRequestRateLimitGroupConfig = make(map[string][2]int)
|
|
||||||
common.SysLog("Model request rate limit group config cleared")
|
common.SysLog("Model request rate limit group config cleared")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -38,37 +31,19 @@ func UpdateModelRequestRateLimitGroupConfig(jsonStr string) error {
|
|||||||
return fmt.Errorf("failed to unmarshal ModelRequestRateLimitGroup config: %w", err)
|
return fmt.Errorf("failed to unmarshal ModelRequestRateLimitGroup config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验配置值
|
ModelRequestRateLimitGroup = newConfig
|
||||||
for group, limits := range newConfig {
|
|
||||||
if len(limits) != 2 {
|
|
||||||
return fmt.Errorf("invalid config for group '%s': limits array length must be 2", group)
|
|
||||||
}
|
|
||||||
if limits[1] <= 0 { // successCount must be greater than 0
|
|
||||||
return fmt.Errorf("invalid config for group '%s': successCount (limits[1]) must be greater than 0", group)
|
|
||||||
}
|
|
||||||
if limits[0] < 0 { // totalCount can be 0 (no limit) or positive
|
|
||||||
return fmt.Errorf("invalid config for group '%s': totalCount (limits[0]) cannot be negative", group)
|
|
||||||
}
|
|
||||||
if limits[0] > 0 && limits[0] < limits[1] { // If totalCount is set, it must be >= successCount
|
|
||||||
return fmt.Errorf("invalid config for group '%s': totalCount (limits[0]) must be greater than or equal to successCount (limits[1]) when totalCount > 0", group)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ModelRequestRateLimitGroupConfig = newConfig
|
|
||||||
common.SysLog("Model request rate limit group config updated")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGroupRateLimit 安全地获取指定用户组的速率限制值
|
|
||||||
func GetGroupRateLimit(group string) (totalCount, successCount int, found bool) {
|
func GetGroupRateLimit(group string) (totalCount, successCount int, found bool) {
|
||||||
ModelRequestRateLimitGroupMutex.RLock()
|
ModelRequestRateLimitGroupMutex.RLock()
|
||||||
defer ModelRequestRateLimitGroupMutex.RUnlock()
|
defer ModelRequestRateLimitGroupMutex.RUnlock()
|
||||||
|
|
||||||
if ModelRequestRateLimitGroupConfig == nil {
|
if ModelRequestRateLimitGroup == nil {
|
||||||
return 0, 0, false // 配置尚未初始化
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
limits, found := ModelRequestRateLimitGroupConfig[group]
|
limits, found := ModelRequestRateLimitGroup[group]
|
||||||
if !found {
|
if !found {
|
||||||
return 0, 0, false
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,59 +9,59 @@ import RequestRateLimit from '../pages/Setting/RateLimit/SettingsRequestRateLimi
|
|||||||
const RateLimitSetting = () => {
|
const RateLimitSetting = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
let [inputs, setInputs] = useState({
|
let [inputs, setInputs] = useState({
|
||||||
ModelRequestRateLimitEnabled: false,
|
ModelRequestRateLimitEnabled: false,
|
||||||
ModelRequestRateLimitCount: 0,
|
ModelRequestRateLimitCount: 0,
|
||||||
ModelRequestRateLimitSuccessCount: 1000,
|
ModelRequestRateLimitSuccessCount: 1000,
|
||||||
ModelRequestRateLimitDurationMinutes: 1,
|
ModelRequestRateLimitDurationMinutes: 1,
|
||||||
ModelRequestRateLimitGroup: {},
|
ModelRequestRateLimitGroup: '{}',
|
||||||
});
|
});
|
||||||
|
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const getOptions = async () => {
|
const getOptions = async () => {
|
||||||
const res = await API.get('/api/option/');
|
const res = await API.get('/api/option/');
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
let newInputs = {};
|
let newInputs = {};
|
||||||
data.forEach((item) => {
|
data.forEach((item) => {
|
||||||
if (item.key.endsWith('Enabled')) {
|
// 检查 key 是否在初始 inputs 中定义
|
||||||
newInputs[item.key] = item.value === 'true' ? true : false;
|
if (Object.prototype.hasOwnProperty.call(inputs, item.key)) {
|
||||||
} else {
|
if (item.key.endsWith('Enabled')) {
|
||||||
newInputs[item.key] = item.value;
|
newInputs[item.key] = item.value === 'true';
|
||||||
}
|
} else {
|
||||||
});
|
newInputs[item.key] = item.value;
|
||||||
|
}
|
||||||
setInputs(newInputs);
|
}
|
||||||
} else {
|
});
|
||||||
showError(message);
|
setInputs(newInputs);
|
||||||
}
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
async function onRefresh() {
|
async function onRefresh() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await getOptions();
|
await getOptions();
|
||||||
// showSuccess('刷新成功');
|
} catch (error) {
|
||||||
} catch (error) {
|
showError('刷新失败');
|
||||||
showError('刷新失败');
|
} finally {
|
||||||
} finally {
|
setLoading(false);
|
||||||
setLoading(false);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onRefresh();
|
onRefresh();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Spin spinning={loading} size='large'>
|
<Spin spinning={loading} size='large'>
|
||||||
{/* AI请求速率限制 */}
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<Card style={{ marginTop: '10px' }}>
|
<RequestRateLimit options={inputs} refresh={onRefresh} />
|
||||||
<RequestRateLimit options={inputs} refresh={onRefresh} />
|
</Card>
|
||||||
</Card>
|
</Spin>
|
||||||
</Spin>
|
</>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RateLimitSetting;
|
export default RateLimitSetting;
|
||||||
|
|||||||
@@ -14,209 +14,201 @@ export default function RequestRateLimit(props) {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
ModelRequestRateLimitEnabled: false,
|
ModelRequestRateLimitEnabled: false,
|
||||||
ModelRequestRateLimitCount: -1,
|
ModelRequestRateLimitCount: -1,
|
||||||
ModelRequestRateLimitSuccessCount: 1000,
|
ModelRequestRateLimitSuccessCount: 1000,
|
||||||
ModelRequestRateLimitDurationMinutes: 1,
|
ModelRequestRateLimitDurationMinutes: 1,
|
||||||
ModelRequestRateLimitGroup: '{}', // 添加新字段并设置默认值
|
ModelRequestRateLimitGroup: '{}',
|
||||||
});
|
});
|
||||||
const refForm = useRef();
|
const refForm = useRef();
|
||||||
const [inputsRow, setInputsRow] = useState(inputs);
|
const [inputsRow, setInputsRow] = useState(inputs);
|
||||||
|
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
const updateArray = compareObjects(inputs, inputsRow);
|
const updateArray = compareObjects(inputs, inputsRow);
|
||||||
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
|
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
|
||||||
const requestQueue = updateArray.map((item) => {
|
const requestQueue = updateArray.map((item) => {
|
||||||
let value = '';
|
let value = '';
|
||||||
if (typeof inputs[item.key] === 'boolean') {
|
if (typeof inputs[item.key] === 'boolean') {
|
||||||
value = String(inputs[item.key]);
|
value = String(inputs[item.key]);
|
||||||
} else {
|
} else {
|
||||||
value = inputs[item.key];
|
value = inputs[item.key];
|
||||||
}
|
}
|
||||||
// 校验 ModelRequestRateLimitGroup 是否为有效的 JSON 对象字符串
|
if (item.key === 'ModelRequestRateLimitGroup') {
|
||||||
if (item.key === 'ModelRequestRateLimitGroup') {
|
try {
|
||||||
try {
|
JSON.parse(value);
|
||||||
JSON.parse(value);
|
} catch (e) {
|
||||||
} catch (e) {
|
showError(t('用户组速率限制配置不是有效的 JSON 格式!'));
|
||||||
showError(t('用户组速率限制配置不是有效的 JSON 格式!'));
|
return Promise.reject('Invalid JSON format');
|
||||||
// 阻止请求发送
|
}
|
||||||
return Promise.reject('Invalid JSON format');
|
}
|
||||||
}
|
return API.put('/api/option/', {
|
||||||
}
|
key: item.key,
|
||||||
return API.put('/api/option/', {
|
value,
|
||||||
key: item.key,
|
});
|
||||||
value,
|
});
|
||||||
});
|
|
||||||
});
|
const validRequests = requestQueue.filter(req => req !== null && req !== undefined && typeof req.then === 'function');
|
||||||
|
|
||||||
// 过滤掉无效的请求(例如,无效的 JSON)
|
if (validRequests.length === 0 && requestQueue.length > 0) {
|
||||||
const validRequests = requestQueue.filter(req => req !== null && req !== undefined && typeof req.then === 'function');
|
return;
|
||||||
|
}
|
||||||
if (validRequests.length === 0 && requestQueue.length > 0) {
|
|
||||||
// 如果所有请求都被过滤掉了(因为 JSON 无效),则不继续执行
|
setLoading(true);
|
||||||
return;
|
Promise.all(validRequests)
|
||||||
}
|
.then((res) => {
|
||||||
|
if (validRequests.length === 1) {
|
||||||
setLoading(true);
|
if (res.includes(undefined)) return;
|
||||||
Promise.all(validRequests)
|
} else if (validRequests.length > 1) {
|
||||||
.then((res) => {
|
if (res.includes(undefined))
|
||||||
if (validRequests.length === 1) {
|
return showError(t('部分保存失败,请重试'));
|
||||||
if (res.includes(undefined)) return;
|
}
|
||||||
} else if (validRequests.length > 1) {
|
showSuccess(t('保存成功'));
|
||||||
if (res.includes(undefined))
|
props.refresh();
|
||||||
return showError(t('部分保存失败,请重试'));
|
setInputsRow(structuredClone(inputs));
|
||||||
}
|
})
|
||||||
showSuccess(t('保存成功'));
|
.catch((error) => {
|
||||||
props.refresh();
|
if (error !== 'Invalid JSON format') {
|
||||||
// 更新 inputsRow 以反映保存后的状态
|
showError(t('保存失败,请重试'));
|
||||||
setInputsRow(structuredClone(inputs));
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.finally(() => {
|
||||||
// 检查是否是由于无效 JSON 导致的错误
|
setLoading(false);
|
||||||
if (error !== 'Invalid JSON format') {
|
});
|
||||||
showError(t('保存失败,请重试'));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentInputs = {};
|
const currentInputs = {};
|
||||||
for (let key in props.options) {
|
for (let key in props.options) {
|
||||||
if (Object.keys(inputs).includes(key)) {
|
if (Object.prototype.hasOwnProperty.call(inputs, key)) { // 使用 hasOwnProperty 检查
|
||||||
currentInputs[key] = props.options[key];
|
currentInputs[key] = props.options[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setInputs(currentInputs);
|
setInputs(currentInputs);
|
||||||
setInputsRow(structuredClone(currentInputs));
|
setInputsRow(structuredClone(currentInputs));
|
||||||
// 检查 refForm.current 是否存在
|
if (refForm.current) {
|
||||||
if (refForm.current) {
|
refForm.current.setValues(currentInputs);
|
||||||
refForm.current.setValues(currentInputs);
|
}
|
||||||
}
|
}, [props.options]);
|
||||||
}, [props.options]); // 依赖项保持不变,因为 inputs 状态的结构已固定
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<Form
|
<Form
|
||||||
values={inputs}
|
values={inputs}
|
||||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||||
style={{ marginBottom: 15 }}
|
style={{ marginBottom: 15 }}
|
||||||
>
|
>
|
||||||
<Form.Section text={t('模型请求速率限制')}>
|
<Form.Section text={t('模型请求速率限制')}>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||||
<Form.Switch
|
<Form.Switch
|
||||||
field={'ModelRequestRateLimitEnabled'}
|
field={'ModelRequestRateLimitEnabled'}
|
||||||
label={t('启用用户模型请求速率限制(可能会影响高并发性能)')}
|
label={t('启用用户模型请求速率限制(可能会影响高并发性能)')}
|
||||||
size='default'
|
size='default'
|
||||||
checkedText='|'
|
checkedText='|'
|
||||||
uncheckedText='〇'
|
uncheckedText='〇'
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setInputs({
|
setInputs({
|
||||||
...inputs,
|
...inputs,
|
||||||
ModelRequestRateLimitEnabled: value,
|
ModelRequestRateLimitEnabled: value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||||
<Form.InputNumber
|
<Form.InputNumber
|
||||||
label={t('限制周期')}
|
label={t('限制周期')}
|
||||||
step={1}
|
step={1}
|
||||||
min={0}
|
min={0}
|
||||||
suffix={t('分钟')}
|
suffix={t('分钟')}
|
||||||
extraText={t('频率限制的周期(分钟)')}
|
extraText={t('频率限制的周期(分钟)')}
|
||||||
field={'ModelRequestRateLimitDurationMinutes'}
|
field={'ModelRequestRateLimitDurationMinutes'}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setInputs({
|
setInputs({
|
||||||
...inputs,
|
...inputs,
|
||||||
ModelRequestRateLimitDurationMinutes: String(value),
|
ModelRequestRateLimitDurationMinutes: String(value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||||
<Form.InputNumber
|
<Form.InputNumber
|
||||||
label={t('用户每周期最多请求次数')}
|
label={t('用户每周期最多请求次数')}
|
||||||
step={1}
|
step={1}
|
||||||
min={0}
|
min={0}
|
||||||
suffix={t('次')}
|
suffix={t('次')}
|
||||||
extraText={t('包括失败请求的次数,0代表不限制')}
|
extraText={t('包括失败请求的次数,0代表不限制')}
|
||||||
field={'ModelRequestRateLimitCount'}
|
field={'ModelRequestRateLimitCount'}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setInputs({
|
setInputs({
|
||||||
...inputs,
|
...inputs,
|
||||||
ModelRequestRateLimitCount: String(value),
|
ModelRequestRateLimitCount: String(value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||||
<Form.InputNumber
|
<Form.InputNumber
|
||||||
label={t('用户每周期最多请求完成次数')}
|
label={t('用户每周期最多请求完成次数')}
|
||||||
step={1}
|
step={1}
|
||||||
min={1}
|
min={1}
|
||||||
suffix={t('次')}
|
suffix={t('次')}
|
||||||
extraText={t('只包括请求成功的次数')}
|
extraText={t('只包括请求成功的次数')}
|
||||||
field={'ModelRequestRateLimitSuccessCount'}
|
field={'ModelRequestRateLimitSuccessCount'}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setInputs({
|
setInputs({
|
||||||
...inputs,
|
...inputs,
|
||||||
ModelRequestRateLimitSuccessCount: String(value),
|
ModelRequestRateLimitSuccessCount: String(value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{/* 用户组速率限制配置项 */}
|
<Row style={{ marginTop: 15 }}>
|
||||||
<Row>
|
<Col span={24}>
|
||||||
<Col span={24}>
|
<Form.TextArea
|
||||||
<Form.TextArea
|
label={t('用户组速率限制 (JSON)')}
|
||||||
label={t('用户组速率限制 (JSON)')}
|
field={'ModelRequestRateLimitGroup'}
|
||||||
field={'ModelRequestRateLimitGroup'}
|
placeholder={t(
|
||||||
placeholder={t( // 更新 placeholder
|
'请输入 JSON 格式的用户组限制,例如:\n{\n "default": [200, 100],\n "vip": [1000, 500]\n}',
|
||||||
'请输入 JSON 格式的用户组限制,例如:\n{\n "default": [200, 100],\n "vip": [1000, 500]\n}',
|
)}
|
||||||
)}
|
extraText={
|
||||||
extraText={ // 更新 extraText
|
<div>
|
||||||
<div>
|
<p>{t('说明:')}</p>
|
||||||
<p>{t('说明:')}</p>
|
<ul>
|
||||||
<ul>
|
<li>{t('使用 JSON 对象格式,键为用户组名 (字符串),值为包含两个整数的数组 [总次数限制, 成功次数限制]。')}</li>
|
||||||
<li>{t('使用 JSON 对象格式,键为用户组名 (字符串),值为包含两个整数的数组 [总次数限制, 成功次数限制]。')}</li>
|
<li>{t('总次数限制: 周期内允许的总请求次数 (含失败),0 代表不限制。')}</li>
|
||||||
<li>{t('总次数限制: 周期内允许的总请求次数 (含失败),0 代表不限制。')}</li>
|
<li>{t('成功次数限制: 周期内允许的成功请求次数 (HTTP < 400),必须大于 0。')}</li>
|
||||||
<li>{t('成功次数限制: 周期内允许的成功请求次数 (HTTP < 400),必须大于 0。')}</li>
|
<li>{t('此配置将优先于上方的全局限制设置。')}</li>
|
||||||
<li>{t('此配置将优先于上方的全局限制设置。')}</li>
|
<li>{t('未在此处配置的用户组将使用全局限制。')}</li>
|
||||||
<li>{t('未在此处配置的用户组将使用全局限制。')}</li>
|
<li>{t('限制周期统一使用上方配置的“限制周期”值。')}</li>
|
||||||
<li>{t('限制周期统一使用上方配置的“限制周期”值。')}</li>
|
<li>{t('输入无效的 JSON 将无法保存。')}</li>
|
||||||
<li>{t('输入无效的 JSON 将无法保存。')}</li>
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
autosize={{ minRows: 5, maxRows: 15 }}
|
||||||
autosize={{ minRows: 5, maxRows: 15 }}
|
style={{ fontFamily: 'monospace' }}
|
||||||
style={{ fontFamily: 'monospace' }}
|
onChange={(value) => {
|
||||||
onChange={(value) => {
|
setInputs({
|
||||||
setInputs({
|
...inputs,
|
||||||
...inputs,
|
ModelRequestRateLimitGroup: value,
|
||||||
ModelRequestRateLimitGroup: value, // 直接更新字符串值
|
});
|
||||||
});
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</Col>
|
||||||
</Col>
|
</Row>
|
||||||
</Row>
|
<Row style={{ marginTop: 15 }}>
|
||||||
<Row style={{ marginTop: 15 }}>
|
<Button size='default' onClick={onSubmit}>
|
||||||
<Button size='default' onClick={onSubmit}>
|
{t('保存模型速率限制')}
|
||||||
{t('保存模型速率限制')}
|
</Button>
|
||||||
</Button>
|
</Row>
|
||||||
</Row>
|
</Form.Section>
|
||||||
</Form.Section>
|
</Form>
|
||||||
</Form>
|
</Spin>
|
||||||
</Spin>
|
</>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user