mirror of
https://gh-proxy.org/https://github.com/klinsmaya/new-api.git
synced 2026-11-08 01:17:09 +08:00
* feat(email): add Resend delivery provider Add Resend as an alternative to SMTP with separately editable sender address and name, credential-safe configuration status, and validation before activation. Preserve the target repository's email notification and balance reminder settings. Only Resend changes are transplanted from hicoderai/new-api; no landing, legal-document, model-group, pricing, branding, or dependency changes are included. Validation: Go common/controller email regression tests; 19 frontend email and notification tests; frontend production build; tsgo typecheck; touched-file lint. Frontend validation uses the target lockfile's Axios 1.18.1 with local cached tools. Security references: OWASP Authentication, Session Management, and Forgot Password Cheat Sheets. Tests cover credential non-disclosure, invalid activation, sanitized transport errors, and no automatic retry or fallback; token and session flows are unchanged. https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html * fix(email): log Resend rejections and translate Resend errors Resend's reason for a rejected or failed send now goes to the server log. People who asked for the email still see the short error. Resend and email method errors are translatable messages in all seven languages. The email settings hints are shorter and reuse the existing key hint. The new frontend files carry the license header, the sender name check passes lint, and test fixtures use neutral names. * fix(email): check Resend settings while Resend is active Saving the sender address or API key now checks the whole Resend setup while Resend sends the mail. A typo the form accepts, such as [email protected], is rejected instead of breaking every send. Resend's error text and network errors are quoted in the server log. --------- Co-authored-by: CaIon <[email protected]>
751 lines
30 KiB
Go
751 lines
30 KiB
Go
package model
|
|
|
|
import (
|
|
"maps"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/pkg/jsplugin"
|
|
"github.com/QuantumNous/new-api/setting"
|
|
"github.com/QuantumNous/new-api/setting/config"
|
|
"github.com/QuantumNous/new-api/setting/operation_setting"
|
|
"github.com/QuantumNous/new-api/setting/performance_setting"
|
|
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
|
"github.com/QuantumNous/new-api/setting/system_setting"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
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["TaskPluginEnabled"] = strconv.FormatBool(constant.TaskPluginEnabled)
|
|
jsplugin.DefaultRegistry.SetEnabled(constant.TaskPluginEnabled)
|
|
common.OptionMap[setting.TaskPluginMarketplaceSourcesKey] = setting.TaskPluginMarketplaceSources2JsonString()
|
|
common.OptionMap[setting.TaskPluginDisabledFactoryKeysKey] = "[]"
|
|
jsplugin.DefaultRegistry.SetDisabledFactoryKeys(nil)
|
|
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["EmailProvider"] = "smtp"
|
|
common.OptionMap["ResendApiKey"] = ""
|
|
common.OptionMap["ResendFrom"] = ""
|
|
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["SMTPStartTLSEnabled"] = strconv.FormatBool(common.SMTPStartTLSEnabled)
|
|
common.OptionMap["SMTPInsecureSkipVerify"] = strconv.FormatBool(common.SMTPInsecureSkipVerify)
|
|
common.OptionMap["SMTPForceAuthLogin"] = strconv.FormatBool(common.SMTPForceAuthLogin)
|
|
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["TaskPublicAddress"] = system_setting.TaskPublicAddress
|
|
common.OptionMap["WorkerUrl"] = system_setting.WorkerUrl
|
|
common.OptionMap["WorkerValidKey"] = system_setting.WorkerValidKey
|
|
common.OptionMap["WorkerAllowHttpImageRequestEnabled"] = strconv.FormatBool(system_setting.WorkerAllowHttpImageRequestEnabled)
|
|
common.OptionMap["PayAddress"] = ""
|
|
common.OptionMap["CustomCallbackAddress"] = ""
|
|
common.OptionMap["EpayId"] = ""
|
|
common.OptionMap["EpayKey"] = ""
|
|
common.OptionMap["Price"] = strconv.FormatFloat(operation_setting.Price, 'f', -1, 64)
|
|
common.OptionMap["USDExchangeRate"] = strconv.FormatFloat(operation_setting.USDExchangeRate, 'f', -1, 64)
|
|
common.OptionMap["MinTopUp"] = strconv.Itoa(operation_setting.MinTopUp)
|
|
common.OptionMap["StripeMinTopUp"] = strconv.Itoa(setting.StripeMinTopUp)
|
|
common.OptionMap["StripeApiSecret"] = setting.StripeApiSecret
|
|
common.OptionMap["StripeWebhookSecret"] = setting.StripeWebhookSecret
|
|
common.OptionMap["StripePriceId"] = setting.StripePriceId
|
|
common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(setting.StripeUnitPrice, 'f', -1, 64)
|
|
common.OptionMap["StripePromotionCodesEnabled"] = strconv.FormatBool(setting.StripePromotionCodesEnabled)
|
|
common.OptionMap["CreemApiKey"] = setting.CreemApiKey
|
|
common.OptionMap["CreemProducts"] = setting.CreemProducts
|
|
common.OptionMap["CreemTestMode"] = strconv.FormatBool(setting.CreemTestMode)
|
|
common.OptionMap["CreemWebhookSecret"] = setting.CreemWebhookSecret
|
|
common.OptionMap["WaffoEnabled"] = strconv.FormatBool(setting.WaffoEnabled)
|
|
common.OptionMap["WaffoApiKey"] = setting.WaffoApiKey
|
|
common.OptionMap["WaffoPrivateKey"] = setting.WaffoPrivateKey
|
|
common.OptionMap["WaffoPublicCert"] = setting.WaffoPublicCert
|
|
common.OptionMap["WaffoSandboxPublicCert"] = setting.WaffoSandboxPublicCert
|
|
common.OptionMap["WaffoSandboxApiKey"] = setting.WaffoSandboxApiKey
|
|
common.OptionMap["WaffoSandboxPrivateKey"] = setting.WaffoSandboxPrivateKey
|
|
common.OptionMap["WaffoSandbox"] = strconv.FormatBool(setting.WaffoSandbox)
|
|
common.OptionMap["WaffoMerchantId"] = setting.WaffoMerchantId
|
|
common.OptionMap["WaffoNotifyUrl"] = setting.WaffoNotifyUrl
|
|
common.OptionMap["WaffoReturnUrl"] = setting.WaffoReturnUrl
|
|
common.OptionMap["WaffoSubscriptionReturnUrl"] = setting.WaffoSubscriptionReturnUrl
|
|
common.OptionMap["WaffoCurrency"] = setting.WaffoCurrency
|
|
common.OptionMap["WaffoUnitPrice"] = strconv.FormatFloat(setting.WaffoUnitPrice, 'f', -1, 64)
|
|
common.OptionMap["WaffoMinTopUp"] = strconv.Itoa(setting.WaffoMinTopUp)
|
|
common.OptionMap["WaffoPayMethods"] = setting.WaffoPayMethods2JsonString()
|
|
common.OptionMap["WaffoPancakeMerchantID"] = setting.WaffoPancakeMerchantID
|
|
common.OptionMap["WaffoPancakePrivateKey"] = setting.WaffoPancakePrivateKey
|
|
common.OptionMap["WaffoPancakeReturnURL"] = setting.WaffoPancakeReturnURL
|
|
common.OptionMap["WaffoPancakeUnitPrice"] = strconv.FormatFloat(setting.WaffoPancakeUnitPrice, 'f', -1, 64)
|
|
common.OptionMap["WaffoPancakeMinTopUp"] = strconv.Itoa(setting.WaffoPancakeMinTopUp)
|
|
common.OptionMap["WaffoPancakeStoreID"] = setting.WaffoPancakeStoreID
|
|
common.OptionMap["WaffoPancakeProductID"] = setting.WaffoPancakeProductID
|
|
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
|
|
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
|
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
|
|
common.OptionMap["DefaultUseAutoGroup"] = strconv.FormatBool(setting.DefaultUseAutoGroup)
|
|
common.OptionMap["MaxTokenAutoGroups"] = strconv.Itoa(setting.GetMaxTokenAutoGroups())
|
|
common.OptionMap["PayMethods"] = operation_setting.PayMethods2JsonString()
|
|
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["QuotaRemindEnabled"] = strconv.FormatBool(common.QuotaRemindEnabled)
|
|
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["ModelRequestRateLimitGroup"] = setting.ModelRequestRateLimitGroup2JSONString()
|
|
common.OptionMap["ModelRatio"] = ratio_setting.ModelRatio2JSONString()
|
|
common.OptionMap["ModelPrice"] = ratio_setting.ModelPrice2JSONString()
|
|
common.OptionMap["CacheRatio"] = ratio_setting.CacheRatio2JSONString()
|
|
common.OptionMap["CreateCacheRatio"] = ratio_setting.CreateCacheRatio2JSONString()
|
|
common.OptionMap["GroupRatio"] = ratio_setting.GroupRatio2JSONString()
|
|
common.OptionMap["GroupGroupRatio"] = ratio_setting.GroupGroupRatio2JSONString()
|
|
common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString()
|
|
common.OptionMap["CompletionRatio"] = ratio_setting.CompletionRatio2JSONString()
|
|
common.OptionMap["ImageRatio"] = ratio_setting.ImageRatio2JSONString()
|
|
common.OptionMap["AudioRatio"] = ratio_setting.AudioRatio2JSONString()
|
|
common.OptionMap["AudioCompletionRatio"] = ratio_setting.AudioCompletionRatio2JSONString()
|
|
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()
|
|
common.OptionMap["AutomaticDisableStatusCodes"] = operation_setting.AutomaticDisableStatusCodesToString()
|
|
common.OptionMap["AutomaticRetryStatusCodes"] = operation_setting.AutomaticRetryStatusCodesToString()
|
|
common.OptionMap["ExposeRatioEnabled"] = strconv.FormatBool(ratio_setting.IsExposeRatioEnabled())
|
|
|
|
// 自动添加所有注册的模型配置
|
|
modelConfigs := config.GlobalConfig.ExportAllConfigs()
|
|
maps.Copy(common.OptionMap, modelConfigs)
|
|
|
|
common.OptionMapRWMutex.Unlock()
|
|
loadOptionsFromDatabase()
|
|
}
|
|
|
|
func loadOptionsFromDatabase() {
|
|
requestPolicyOptionMutex.Lock()
|
|
defer requestPolicyOptionMutex.Unlock()
|
|
defer func() {
|
|
if err := refreshRequestPolicySnapshot(); err != nil {
|
|
common.SysError(common.LogText("invalid request policy: %s", err.Error()))
|
|
}
|
|
}()
|
|
passkeyOptionMutex.Lock()
|
|
defer passkeyOptionMutex.Unlock()
|
|
options, _ := AllOption()
|
|
passkeyOptions := make(map[string]string)
|
|
for _, option := range options {
|
|
if IsPasskeyDomainOption(option.Key) {
|
|
passkeyOptions[option.Key] = option.Value
|
|
continue
|
|
}
|
|
err := updateOptionMap(option.Key, option.Value)
|
|
if err != nil {
|
|
common.SysLog(common.LogText("failed to update option map: %s", err.Error()))
|
|
}
|
|
}
|
|
applyPasskeyDomainOptions(passkeyOptions)
|
|
}
|
|
|
|
func SyncOptions(frequency int) {
|
|
for {
|
|
time.Sleep(time.Duration(frequency) * time.Second)
|
|
common.SysLog(common.LogText("syncing options from database"))
|
|
loadOptionsFromDatabase()
|
|
}
|
|
}
|
|
|
|
func validateOptionValue(key string, value string) error {
|
|
if key == legacyAccessTokenRetireAtKey {
|
|
return errLegacyRetireAtReadOnly
|
|
}
|
|
if err := operation_setting.ValidateQuotaOption(key, value); err != nil {
|
|
return err
|
|
}
|
|
if key == "EmailProvider" || key == "ResendApiKey" || key == "ResendFrom" {
|
|
if key == "EmailProvider" && value != "smtp" && value != "resend" {
|
|
return common.NewMessage("Email sending method must be SMTP or Resend")
|
|
}
|
|
common.OptionMapRWMutex.RLock()
|
|
settings := map[string]string{
|
|
"EmailProvider": common.OptionMap["EmailProvider"],
|
|
"ResendApiKey": common.OptionMap["ResendApiKey"],
|
|
"ResendFrom": common.OptionMap["ResendFrom"],
|
|
}
|
|
common.OptionMapRWMutex.RUnlock()
|
|
settings[key] = value
|
|
// While Resend is the active method, a change must keep settings it can
|
|
// send with. Inactive drafts are checked when Resend is turned on.
|
|
if settings["EmailProvider"] == "resend" {
|
|
return common.ValidateResendConfiguration(settings["ResendApiKey"], settings["ResendFrom"])
|
|
}
|
|
}
|
|
if key == "ResendKeyConfigured" {
|
|
return common.NewMessage("Resend key configuration status is read-only")
|
|
}
|
|
if key == operation_setting.ToolPriceOptionKey {
|
|
return operation_setting.ValidateToolPricesJSON(value)
|
|
}
|
|
if key == operation_setting.ChannelTestConcurrencyOptionKey {
|
|
return operation_setting.ValidateChannelTestConcurrency(value)
|
|
}
|
|
if key == operation_setting.LogMaxRangeDaysOptionKey {
|
|
return operation_setting.ValidateLogMaxRangeDays(value)
|
|
}
|
|
if key == "MaxTokenAutoGroups" {
|
|
return setting.ValidateMaxTokenAutoGroups(value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func UpdateOption(key string, value string) error {
|
|
if IsRequestPolicyOption(key) {
|
|
return UpdateRequestPolicyOptions(map[string]string{key: value})
|
|
}
|
|
if IsPasskeyDomainOption(key) {
|
|
_, err := UpdatePasskeyDomainOptions(map[string]string{key: value}, false, "")
|
|
return err
|
|
}
|
|
if IsModelPricingOption(key) {
|
|
return UpdateModelPricingOptions(map[string]string{key: value})
|
|
}
|
|
if err := validateOptionValue(key, value); err != nil {
|
|
return err
|
|
}
|
|
// 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)
|
|
}
|
|
|
|
// UpdateOptionsBulk persists multiple key/value pairs in a single database
|
|
// transaction, then dispatches them through updateOptionMap in one pass. If
|
|
// any DB write fails the whole transaction rolls back and no in-memory state
|
|
// is touched — safe for callers that must commit a set of related options
|
|
// atomically (e.g. payment gateway binding).
|
|
func UpdateOptionsBulk(values map[string]string) error {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
for key := range values {
|
|
if IsPasskeyDomainOption(key) {
|
|
_, err := UpdatePasskeyDomainOptions(values, false, "")
|
|
return err
|
|
}
|
|
}
|
|
for key, value := range values {
|
|
if err := validateOptionValue(key, value); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var policySnapshot *RequestPolicySnapshot
|
|
for key := range values {
|
|
if IsRequestPolicyOption(key) {
|
|
requestPolicyOptionMutex.Lock()
|
|
defer requestPolicyOptionMutex.Unlock()
|
|
options := maps.Clone(CurrentRequestPolicy().Options)
|
|
for key, value := range values {
|
|
if IsRequestPolicyOption(key) {
|
|
options[key] = value
|
|
}
|
|
}
|
|
var err error
|
|
policySnapshot, err = BuildRequestPolicy(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
err := DB.Transaction(func(tx *gorm.DB) error {
|
|
for k, v := range values {
|
|
option := Option{Key: k}
|
|
if err := tx.FirstOrCreate(&option, Option{Key: k}).Error; err != nil {
|
|
return err
|
|
}
|
|
option.Value = v
|
|
if err := tx.Save(&option).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for k, v := range values {
|
|
if err := updateOptionMap(k, v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if policySnapshot != nil {
|
|
requestPolicySnapshot.Store(policySnapshot)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func updateOptionMap(key string, value string) (err error) {
|
|
if key == retiredThemeOptionKey || key == legacyAccessTokenRetireAtKey {
|
|
common.OptionMapRWMutex.Lock()
|
|
delete(common.OptionMap, key)
|
|
common.OptionMapRWMutex.Unlock()
|
|
return nil
|
|
}
|
|
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" || key == "DefaultUseAutoGroup" || key == "SMTPForceAuthLogin" || key == "SMTPInsecureSkipVerify" {
|
|
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 "QuotaRemindEnabled":
|
|
common.QuotaRemindEnabled = boolValue
|
|
case "LogConsumeEnabled":
|
|
common.LogConsumeEnabled = boolValue
|
|
case "DisplayInCurrencyEnabled":
|
|
// 兼容旧字段:同步到新配置 general_setting.quota_display_type(运行时生效)
|
|
// true -> USD, false -> TOKENS
|
|
newVal := "USD"
|
|
if !boolValue {
|
|
newVal = "TOKENS"
|
|
}
|
|
if cfg := config.GlobalConfig.Get("general_setting"); cfg != nil {
|
|
_ = config.UpdateConfigFromMap(cfg, map[string]string{"quota_display_type": newVal})
|
|
}
|
|
case "DisplayTokenStatEnabled":
|
|
common.DisplayTokenStatEnabled = boolValue
|
|
case "DrawingEnabled":
|
|
common.DrawingEnabled = boolValue
|
|
case "TaskEnabled":
|
|
common.TaskEnabled = boolValue
|
|
case "TaskPluginEnabled":
|
|
constant.TaskPluginEnabled = boolValue
|
|
jsplugin.DefaultRegistry.SetEnabled(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
|
|
case "SMTPStartTLSEnabled":
|
|
common.SMTPStartTLSEnabled = boolValue
|
|
case "SMTPInsecureSkipVerify":
|
|
common.SMTPInsecureSkipVerify = boolValue
|
|
case "SMTPForceAuthLogin":
|
|
common.SMTPForceAuthLogin = boolValue
|
|
case "WorkerAllowHttpImageRequestEnabled":
|
|
system_setting.WorkerAllowHttpImageRequestEnabled = boolValue
|
|
case "DefaultUseAutoGroup":
|
|
setting.DefaultUseAutoGroup = boolValue
|
|
case "ExposeRatioEnabled":
|
|
ratio_setting.SetExposeRatioEnabled(boolValue)
|
|
}
|
|
}
|
|
if key == setting.TaskPluginDisabledFactoryKeysKey {
|
|
jsplugin.DefaultRegistry.SetDisabledFactoryKeys(setting.ParseTaskPluginDisabledFactoryKeys(value))
|
|
}
|
|
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":
|
|
system_setting.ServerAddress = value
|
|
case "TaskPublicAddress":
|
|
system_setting.TaskPublicAddress = value
|
|
case "WorkerUrl":
|
|
system_setting.WorkerUrl = value
|
|
case "WorkerValidKey":
|
|
system_setting.WorkerValidKey = value
|
|
case "PayAddress":
|
|
operation_setting.PayAddress = value
|
|
case "Chats":
|
|
err = setting.UpdateChatsByJsonString(value)
|
|
case "AutoGroups":
|
|
err = setting.UpdateAutoGroupsByJsonString(value)
|
|
case "MaxTokenAutoGroups":
|
|
err = setting.UpdateMaxTokenAutoGroups(value)
|
|
case "CustomCallbackAddress":
|
|
operation_setting.CustomCallbackAddress = value
|
|
case "EpayId":
|
|
operation_setting.EpayId = value
|
|
case "EpayKey":
|
|
operation_setting.EpayKey = value
|
|
case "Price":
|
|
operation_setting.Price, _ = strconv.ParseFloat(value, 64)
|
|
case "USDExchangeRate":
|
|
operation_setting.USDExchangeRate, _ = strconv.ParseFloat(value, 64)
|
|
case "MinTopUp":
|
|
operation_setting.MinTopUp, _ = strconv.Atoi(value)
|
|
case "StripeApiSecret":
|
|
setting.StripeApiSecret = value
|
|
case "StripeWebhookSecret":
|
|
setting.StripeWebhookSecret = value
|
|
case "StripePriceId":
|
|
setting.StripePriceId = value
|
|
case "StripeUnitPrice":
|
|
setting.StripeUnitPrice, _ = strconv.ParseFloat(value, 64)
|
|
case "StripeMinTopUp":
|
|
setting.StripeMinTopUp, _ = strconv.Atoi(value)
|
|
case "StripePromotionCodesEnabled":
|
|
setting.StripePromotionCodesEnabled = value == "true"
|
|
case "CreemApiKey":
|
|
setting.CreemApiKey = value
|
|
case "CreemProducts":
|
|
setting.CreemProducts = value
|
|
case "CreemTestMode":
|
|
setting.CreemTestMode = value == "true"
|
|
case "CreemWebhookSecret":
|
|
setting.CreemWebhookSecret = value
|
|
case "WaffoEnabled":
|
|
setting.WaffoEnabled = value == "true"
|
|
case "WaffoApiKey":
|
|
setting.WaffoApiKey = value
|
|
case "WaffoPrivateKey":
|
|
setting.WaffoPrivateKey = value
|
|
case "WaffoPublicCert":
|
|
setting.WaffoPublicCert = value
|
|
case "WaffoSandboxPublicCert":
|
|
setting.WaffoSandboxPublicCert = value
|
|
case "WaffoSandboxApiKey":
|
|
setting.WaffoSandboxApiKey = value
|
|
case "WaffoSandboxPrivateKey":
|
|
setting.WaffoSandboxPrivateKey = value
|
|
case "WaffoSandbox":
|
|
setting.WaffoSandbox = value == "true"
|
|
case "WaffoMerchantId":
|
|
setting.WaffoMerchantId = value
|
|
case "WaffoNotifyUrl":
|
|
setting.WaffoNotifyUrl = value
|
|
case "WaffoReturnUrl":
|
|
setting.WaffoReturnUrl = value
|
|
case "WaffoSubscriptionReturnUrl":
|
|
setting.WaffoSubscriptionReturnUrl = value
|
|
case "WaffoCurrency":
|
|
setting.WaffoCurrency = value
|
|
case "WaffoUnitPrice":
|
|
setting.WaffoUnitPrice, _ = strconv.ParseFloat(value, 64)
|
|
case "WaffoMinTopUp":
|
|
setting.WaffoMinTopUp, _ = strconv.Atoi(value)
|
|
case "WaffoPancakeMerchantID":
|
|
setting.WaffoPancakeMerchantID = value
|
|
case "WaffoPancakePrivateKey":
|
|
setting.WaffoPancakePrivateKey = value
|
|
case "WaffoPancakeReturnURL":
|
|
setting.WaffoPancakeReturnURL = value
|
|
case "WaffoPancakeStoreID":
|
|
setting.WaffoPancakeStoreID = value
|
|
case "WaffoPancakeProductID":
|
|
setting.WaffoPancakeProductID = value
|
|
case "WaffoPancakeUnitPrice":
|
|
setting.WaffoPancakeUnitPrice, _ = strconv.ParseFloat(value, 64)
|
|
case "WaffoPancakeMinTopUp":
|
|
setting.WaffoPancakeMinTopUp, _ = 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 "LinuxDOMinimumTrustLevel":
|
|
common.LinuxDOMinimumTrustLevel, _ = strconv.Atoi(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 "ModelRequestRateLimitGroup":
|
|
err = setting.UpdateModelRequestRateLimitGroupByJSONString(value)
|
|
case "RetryTimes":
|
|
common.RetryTimes, _ = strconv.Atoi(value)
|
|
case "DataExportInterval":
|
|
common.DataExportInterval, _ = strconv.Atoi(value)
|
|
case "DataExportDefaultTime":
|
|
common.DataExportDefaultTime = value
|
|
case "ModelRatio":
|
|
err = ratio_setting.UpdateModelRatioByJSONString(value)
|
|
case "GroupRatio":
|
|
err = ratio_setting.UpdateGroupRatioByJSONString(value)
|
|
case "GroupGroupRatio":
|
|
err = ratio_setting.UpdateGroupGroupRatioByJSONString(value)
|
|
case "UserUsableGroups":
|
|
err = setting.UpdateUserUsableGroupsByJSONString(value)
|
|
case "CompletionRatio":
|
|
err = ratio_setting.UpdateCompletionRatioByJSONString(value)
|
|
case "ModelPrice":
|
|
err = ratio_setting.UpdateModelPriceByJSONString(value)
|
|
case "CacheRatio":
|
|
err = ratio_setting.UpdateCacheRatioByJSONString(value)
|
|
case "CreateCacheRatio":
|
|
err = ratio_setting.UpdateCreateCacheRatioByJSONString(value)
|
|
case "ImageRatio":
|
|
err = ratio_setting.UpdateImageRatioByJSONString(value)
|
|
case "AudioRatio":
|
|
err = ratio_setting.UpdateAudioRatioByJSONString(value)
|
|
case "AudioCompletionRatio":
|
|
err = ratio_setting.UpdateAudioCompletionRatioByJSONString(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 "AutomaticDisableStatusCodes":
|
|
err = operation_setting.AutomaticDisableStatusCodesFromString(value)
|
|
case "AutomaticRetryStatusCodes":
|
|
err = operation_setting.AutomaticRetryStatusCodesFromString(value)
|
|
case "StreamCacheQueueLength":
|
|
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
|
|
case "PayMethods":
|
|
err = operation_setting.UpdatePayMethodsByJsonString(value)
|
|
case "WaffoPayMethods":
|
|
// WaffoPayMethods is read directly from OptionMap via setting.GetWaffoPayMethods().
|
|
// The value is already stored in OptionMap at the top of this function (line: common.OptionMap[key] = value).
|
|
// No additional in-memory variable to update.
|
|
}
|
|
return err
|
|
}
|
|
|
|
// handleConfigUpdate 处理分层配置更新,返回是否已处理
|
|
func handleConfigUpdate(key, value string) bool {
|
|
if key == operation_setting.ToolPriceOptionKey {
|
|
operation_setting.LoadToolPricesFromJSONString(value)
|
|
return true
|
|
}
|
|
|
|
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)
|
|
|
|
// 特定配置的后处理
|
|
if configName == "performance_setting" {
|
|
performance_setting.UpdateAndSync()
|
|
} else if configName == "billing_setting" {
|
|
InvalidatePricingCache()
|
|
ratio_setting.InvalidateExposedDataCache()
|
|
}
|
|
|
|
return true // 已处理
|
|
}
|