| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839 |
- package service
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "log/slog"
- "math"
- "strconv"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- )
- // OmittedSettingKeys marks setting keys the caller's payload never carried.
- // SystemSettings is a plain struct, so a field the caller omitted arrives as a
- // zero value and is indistinguishable from a deliberate clear. Listing the key
- // here drops it from the write, leaving the stored value in place.
- //
- // A nil or empty set keeps whole-document semantics: every key is written.
- type OmittedSettingKeys map[string]struct{}
- func (o OmittedSettingKeys) dropFrom(updates map[string]string) {
- for key := range o {
- delete(updates, key)
- }
- }
- // UpdateSettings 更新系统设置
- func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSettings) error {
- return s.UpdateSettingsOmitting(ctx, settings, nil)
- }
- // UpdateSettingsOmitting persists system settings, leaving the keys in omitted
- // at their stored value.
- func (s *SettingService) UpdateSettingsOmitting(ctx context.Context, settings *SystemSettings, omitted OmittedSettingKeys) error {
- updates, err := s.buildSystemSettingsUpdates(ctx, settings)
- if err != nil {
- return err
- }
- omitted.dropFrom(updates)
- if err := s.settingRepo.SetMultiple(ctx, updates); err != nil {
- return err
- }
- s.refreshCachedSettingsAfterWrite(ctx, settings, omitted)
- return nil
- }
- // UpdateSettingsWithAuthSourceDefaults persists system settings and auth-source defaults in a single write.
- func (s *SettingService) UpdateSettingsWithAuthSourceDefaults(ctx context.Context, settings *SystemSettings, authDefaults *AuthSourceDefaultSettings) error {
- return s.UpdateSettingsWithAuthSourceDefaultsOmitting(ctx, settings, authDefaults, nil)
- }
- // UpdateSettingsWithAuthSourceDefaultsOmitting persists system settings and
- // auth-source defaults in a single write, leaving the keys in omitted at their
- // stored value.
- func (s *SettingService) UpdateSettingsWithAuthSourceDefaultsOmitting(ctx context.Context, settings *SystemSettings, authDefaults *AuthSourceDefaultSettings, omitted OmittedSettingKeys) error {
- updates, err := s.buildSystemSettingsUpdates(ctx, settings)
- if err != nil {
- return err
- }
- authSourceUpdates, err := s.buildAuthSourceDefaultUpdates(ctx, authDefaults)
- if err != nil {
- return err
- }
- for key, value := range authSourceUpdates {
- updates[key] = value
- }
- omitted.dropFrom(updates)
- if err := s.settingRepo.SetMultiple(ctx, updates); err != nil {
- return err
- }
- s.refreshCachedSettingsAfterWrite(ctx, settings, omitted)
- return nil
- }
- // refreshCachedSettingsAfterWrite keeps the in-process caches in step with the
- // write that just landed. A partial payload carries zero values for the fields
- // it omitted, so in that case the caches are rebuilt from storage rather than
- // from the request struct.
- func (s *SettingService) refreshCachedSettingsAfterWrite(ctx context.Context, settings *SystemSettings, omitted OmittedSettingKeys) {
- if len(omitted) == 0 {
- s.refreshCachedSettings(settings)
- return
- }
- stored, err := s.GetAllSettings(ctx)
- if err != nil {
- slog.Warn("refresh cached settings after partial update failed", "error", err)
- return
- }
- s.refreshCachedSettings(stored)
- }
- func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, settings *SystemSettings) (map[string]string, error) {
- if err := s.validateDefaultSubscriptionGroups(ctx, settings.DefaultSubscriptions); err != nil {
- return nil, err
- }
- normalizedWhitelist, err := NormalizeRegistrationEmailSuffixWhitelist(settings.RegistrationEmailSuffixWhitelist)
- if err != nil {
- return nil, infraerrors.BadRequest("INVALID_REGISTRATION_EMAIL_SUFFIX_WHITELIST", err.Error())
- }
- if normalizedWhitelist == nil {
- normalizedWhitelist = []string{}
- }
- settings.RegistrationEmailSuffixWhitelist = normalizedWhitelist
- normalizedForwardedClientIPHeaders, err := config.NormalizeForwardedClientIPHeaders(settings.ForwardedClientIPHeaders)
- if err != nil {
- return nil, infraerrors.BadRequest("INVALID_FORWARDED_CLIENT_IP_HEADERS", err.Error())
- }
- settings.ForwardedClientIPHeaders = normalizedForwardedClientIPHeaders
- alipaySource, err := normalizeVisibleMethodSettingSource("alipay", settings.PaymentVisibleMethodAlipaySource, settings.PaymentVisibleMethodAlipayEnabled)
- if err != nil {
- return nil, err
- }
- wxpaySource, err := normalizeVisibleMethodSettingSource("wxpay", settings.PaymentVisibleMethodWxpaySource, settings.PaymentVisibleMethodWxpayEnabled)
- if err != nil {
- return nil, err
- }
- if err := s.normalizeOpenAIAdvancedSchedulerOverrides(settings); err != nil {
- return nil, err
- }
- settings.PaymentVisibleMethodAlipaySource = alipaySource
- settings.PaymentVisibleMethodWxpaySource = wxpaySource
- settings.WeChatConnectAppID = strings.TrimSpace(settings.WeChatConnectAppID)
- settings.WeChatConnectAppSecret = strings.TrimSpace(settings.WeChatConnectAppSecret)
- settings.WeChatConnectOpenAppID = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectOpenAppID, settings.WeChatConnectAppID))
- settings.WeChatConnectOpenAppSecret = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectOpenAppSecret, settings.WeChatConnectAppSecret))
- settings.WeChatConnectMPAppID = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectMPAppID, settings.WeChatConnectAppID))
- settings.WeChatConnectMPAppSecret = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectMPAppSecret, settings.WeChatConnectAppSecret))
- settings.WeChatConnectMobileAppID = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectMobileAppID, settings.WeChatConnectAppID))
- settings.WeChatConnectMobileAppSecret = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectMobileAppSecret, settings.WeChatConnectAppSecret))
- settings.WeChatConnectMode = normalizeWeChatConnectStoredMode(
- settings.WeChatConnectOpenEnabled,
- settings.WeChatConnectMPEnabled,
- settings.WeChatConnectMobileEnabled,
- settings.WeChatConnectMode,
- )
- settings.WeChatConnectScopes = normalizeWeChatConnectScopeSetting(settings.WeChatConnectScopes, settings.WeChatConnectMode)
- settings.WeChatConnectRedirectURL = strings.TrimSpace(settings.WeChatConnectRedirectURL)
- settings.WeChatConnectFrontendRedirectURL = strings.TrimSpace(settings.WeChatConnectFrontendRedirectURL)
- if settings.WeChatConnectFrontendRedirectURL == "" {
- settings.WeChatConnectFrontendRedirectURL = defaultWeChatConnectFrontend
- }
- settings.GitHubOAuthRedirectURL = strings.TrimSpace(settings.GitHubOAuthRedirectURL)
- settings.GitHubOAuthFrontendRedirectURL = strings.TrimSpace(settings.GitHubOAuthFrontendRedirectURL)
- if settings.GitHubOAuthFrontendRedirectURL == "" {
- settings.GitHubOAuthFrontendRedirectURL = defaultGitHubOAuthFrontend
- }
- settings.GoogleOAuthRedirectURL = strings.TrimSpace(settings.GoogleOAuthRedirectURL)
- settings.GoogleOAuthFrontendRedirectURL = strings.TrimSpace(settings.GoogleOAuthFrontendRedirectURL)
- if settings.GoogleOAuthFrontendRedirectURL == "" {
- settings.GoogleOAuthFrontendRedirectURL = defaultGoogleOAuthFrontend
- }
- updates := make(map[string]string)
- // 注册设置
- updates[SettingKeyRegistrationEnabled] = strconv.FormatBool(settings.RegistrationEnabled)
- updates[SettingKeyEmailVerifyEnabled] = strconv.FormatBool(settings.EmailVerifyEnabled)
- registrationEmailSuffixWhitelistJSON, err := json.Marshal(settings.RegistrationEmailSuffixWhitelist)
- if err != nil {
- return nil, fmt.Errorf("marshal registration email suffix whitelist: %w", err)
- }
- updates[SettingKeyRegistrationEmailSuffixWhitelist] = string(registrationEmailSuffixWhitelistJSON)
- updates[SettingKeyRegistrationEmailDomainQuotaEnabled] = strconv.FormatBool(settings.RegistrationEmailDomainQuotaEnabled)
- updates[SettingKeyPromoCodeEnabled] = strconv.FormatBool(settings.PromoCodeEnabled)
- updates[SettingKeyPasswordResetEnabled] = strconv.FormatBool(settings.PasswordResetEnabled)
- updates[SettingKeyFrontendURL] = settings.FrontendURL
- updates[SettingKeyInvitationCodeEnabled] = strconv.FormatBool(settings.InvitationCodeEnabled)
- updates[SettingKeyTotpEnabled] = strconv.FormatBool(settings.TotpEnabled)
- updates[SettingKeyPasskeyEnabled] = strconv.FormatBool(settings.PasskeyEnabled)
- updates[SettingKeySessionBindingEnabled] = strconv.FormatBool(settings.SessionBindingEnabled)
- updates[SettingKeyStepUpEnabled] = strconv.FormatBool(settings.StepUpEnabled)
- updates[SettingKeyAuditLogRetentionDays] = strconv.Itoa(settings.AuditLogRetentionDays)
- settings.LoginAgreementMode = normalizeLoginAgreementMode(settings.LoginAgreementMode)
- settings.LoginAgreementUpdatedAt = strings.TrimSpace(settings.LoginAgreementUpdatedAt)
- if settings.LoginAgreementUpdatedAt == "" {
- settings.LoginAgreementUpdatedAt = defaultLoginAgreementDate
- }
- loginAgreementDocumentsJSON, err := marshalLoginAgreementDocuments(settings.LoginAgreementDocuments)
- if err != nil {
- return nil, err
- }
- updates[SettingKeyLoginAgreementEnabled] = strconv.FormatBool(settings.LoginAgreementEnabled)
- updates[SettingKeyLoginAgreementMode] = settings.LoginAgreementMode
- updates[SettingKeyLoginAgreementUpdatedAt] = settings.LoginAgreementUpdatedAt
- updates[SettingKeyLoginAgreementDocuments] = loginAgreementDocumentsJSON
- // 邮件服务设置(只有非空才更新密码)
- updates[SettingKeySMTPHost] = settings.SMTPHost
- updates[SettingKeySMTPPort] = strconv.Itoa(settings.SMTPPort)
- updates[SettingKeySMTPUsername] = settings.SMTPUsername
- if settings.SMTPPassword != "" {
- updates[SettingKeySMTPPassword] = settings.SMTPPassword
- }
- updates[SettingKeySMTPFrom] = settings.SMTPFrom
- updates[SettingKeySMTPFromName] = settings.SMTPFromName
- updates[SettingKeySMTPUseTLS] = strconv.FormatBool(settings.SMTPUseTLS)
- // Cloudflare Turnstile 设置(只有非空才更新密钥)
- updates[SettingKeyTurnstileEnabled] = strconv.FormatBool(settings.TurnstileEnabled)
- updates[SettingKeyTurnstileSiteKey] = settings.TurnstileSiteKey
- if settings.TurnstileSecretKey != "" {
- updates[SettingKeyTurnstileSecretKey] = settings.TurnstileSecretKey
- }
- updates[SettingKeyTencentCaptchaEnabled] = strconv.FormatBool(settings.TencentCaptchaEnabled)
- updates[SettingKeyTencentCaptchaAppID] = settings.TencentCaptchaAppID
- if settings.TencentCaptchaAppSecretKey != "" {
- updates[SettingKeyTencentCaptchaAppSecretKey] = settings.TencentCaptchaAppSecretKey
- }
- if settings.TencentCaptchaCloudSecretID != "" {
- updates[SettingKeyTencentCaptchaCloudSecretID] = settings.TencentCaptchaCloudSecretID
- }
- if settings.TencentCaptchaCloudSecretKey != "" {
- updates[SettingKeyTencentCaptchaCloudSecretKey] = settings.TencentCaptchaCloudSecretKey
- }
- updates[SettingKeyTencentCaptchaRegion] = normalizeTencentCaptchaRegion(settings.TencentCaptchaRegion)
- // 阿里云验证码 2.0 设置(只有非空才更新密钥)
- updates[SettingKeyAliyunCaptchaEnabled] = strconv.FormatBool(settings.AliyunCaptchaEnabled)
- updates[SettingKeyAliyunCaptchaAccessKeyID] = settings.AliyunCaptchaAccessKeyID
- if settings.AliyunCaptchaAccessKeySecret != "" {
- updates[SettingKeyAliyunCaptchaAccessKeySecret] = settings.AliyunCaptchaAccessKeySecret
- }
- updates[SettingKeyAliyunCaptchaSceneID] = settings.AliyunCaptchaSceneID
- updates[SettingKeyAliyunCaptchaPrefix] = settings.AliyunCaptchaPrefix
- updates[SettingKeyAliyunCaptchaRegion] = normalizeAliyunCaptchaRegion(settings.AliyunCaptchaRegion)
- updates[SettingKeyAPIKeyACLTrustForwardedIP] = strconv.FormatBool(settings.APIKeyACLTrustForwardedIP)
- forwardedClientIPHeadersJSON, err := json.Marshal(settings.ForwardedClientIPHeaders)
- if err != nil {
- return nil, fmt.Errorf("marshal forwarded client IP headers: %w", err)
- }
- updates[SettingKeyForwardedClientIPHeaders] = string(forwardedClientIPHeadersJSON)
- // LinuxDo Connect OAuth 登录
- updates[SettingKeyLinuxDoConnectEnabled] = strconv.FormatBool(settings.LinuxDoConnectEnabled)
- updates[SettingKeyLinuxDoConnectClientID] = settings.LinuxDoConnectClientID
- updates[SettingKeyLinuxDoConnectRedirectURL] = settings.LinuxDoConnectRedirectURL
- if settings.LinuxDoConnectClientSecret != "" {
- updates[SettingKeyLinuxDoConnectClientSecret] = settings.LinuxDoConnectClientSecret
- }
- // DingTalk Connect OAuth 登录
- updates[SettingKeyDingTalkConnectEnabled] = strconv.FormatBool(settings.DingTalkConnectEnabled)
- updates[SettingKeyDingTalkConnectClientID] = settings.DingTalkConnectClientID
- updates[SettingKeyDingTalkConnectRedirectURL] = settings.DingTalkConnectRedirectURL
- if settings.DingTalkConnectClientSecret != "" {
- updates[SettingKeyDingTalkConnectClientSecret] = settings.DingTalkConnectClientSecret
- }
- updates[SettingKeyDingTalkConnectCorpRestrictionPolicy] = settings.DingTalkConnectCorpRestrictionPolicy
- updates[SettingKeyDingTalkConnectInternalCorpID] = settings.DingTalkConnectInternalCorpID
- updates[SettingKeyDingTalkConnectBypassRegistration] = strconv.FormatBool(settings.DingTalkConnectBypassRegistration)
- updates[SettingKeyDingTalkConnectSyncCorpEmail] = strconv.FormatBool(settings.DingTalkConnectSyncCorpEmail)
- updates[SettingKeyDingTalkConnectSyncDisplayName] = strconv.FormatBool(settings.DingTalkConnectSyncDisplayName)
- updates[SettingKeyDingTalkConnectSyncDept] = strconv.FormatBool(settings.DingTalkConnectSyncDept)
- updates[SettingKeyDingTalkConnectSyncCorpEmailAttrKey] = settings.DingTalkConnectSyncCorpEmailAttrKey
- updates[SettingKeyDingTalkConnectSyncDisplayNameAttrKey] = settings.DingTalkConnectSyncDisplayNameAttrKey
- updates[SettingKeyDingTalkConnectSyncDeptAttrKey] = settings.DingTalkConnectSyncDeptAttrKey
- updates[SettingKeyDingTalkConnectSyncCorpEmailAttrName] = settings.DingTalkConnectSyncCorpEmailAttrName
- updates[SettingKeyDingTalkConnectSyncDisplayNameAttrName] = settings.DingTalkConnectSyncDisplayNameAttrName
- updates[SettingKeyDingTalkConnectSyncDeptAttrName] = settings.DingTalkConnectSyncDeptAttrName
- // Generic OIDC OAuth 登录
- updates[SettingKeyOIDCConnectEnabled] = strconv.FormatBool(settings.OIDCConnectEnabled)
- updates[SettingKeyOIDCConnectProviderName] = settings.OIDCConnectProviderName
- updates[SettingKeyOIDCConnectClientID] = settings.OIDCConnectClientID
- updates[SettingKeyOIDCConnectIssuerURL] = settings.OIDCConnectIssuerURL
- updates[SettingKeyOIDCConnectDiscoveryURL] = settings.OIDCConnectDiscoveryURL
- updates[SettingKeyOIDCConnectAuthorizeURL] = settings.OIDCConnectAuthorizeURL
- updates[SettingKeyOIDCConnectTokenURL] = settings.OIDCConnectTokenURL
- updates[SettingKeyOIDCConnectUserInfoURL] = settings.OIDCConnectUserInfoURL
- updates[SettingKeyOIDCConnectJWKSURL] = settings.OIDCConnectJWKSURL
- updates[SettingKeyOIDCConnectScopes] = settings.OIDCConnectScopes
- updates[SettingKeyOIDCConnectRedirectURL] = settings.OIDCConnectRedirectURL
- updates[SettingKeyOIDCConnectFrontendRedirectURL] = settings.OIDCConnectFrontendRedirectURL
- updates[SettingKeyOIDCConnectTokenAuthMethod] = settings.OIDCConnectTokenAuthMethod
- updates[SettingKeyOIDCConnectUsePKCE] = strconv.FormatBool(settings.OIDCConnectUsePKCE)
- updates[SettingKeyOIDCConnectValidateIDToken] = strconv.FormatBool(settings.OIDCConnectValidateIDToken)
- updates[SettingKeyOIDCConnectAllowedSigningAlgs] = settings.OIDCConnectAllowedSigningAlgs
- updates[SettingKeyOIDCConnectClockSkewSeconds] = strconv.Itoa(settings.OIDCConnectClockSkewSeconds)
- updates[SettingKeyOIDCConnectRequireEmailVerified] = strconv.FormatBool(settings.OIDCConnectRequireEmailVerified)
- updates[SettingKeyOIDCConnectUserInfoEmailPath] = settings.OIDCConnectUserInfoEmailPath
- updates[SettingKeyOIDCConnectUserInfoIDPath] = settings.OIDCConnectUserInfoIDPath
- updates[SettingKeyOIDCConnectUserInfoUsernamePath] = settings.OIDCConnectUserInfoUsernamePath
- if settings.OIDCConnectClientSecret != "" {
- updates[SettingKeyOIDCConnectClientSecret] = settings.OIDCConnectClientSecret
- }
- // GitHub / Google 邮箱快捷登录
- updates[SettingKeyGitHubOAuthEnabled] = strconv.FormatBool(settings.GitHubOAuthEnabled)
- updates[SettingKeyGitHubOAuthClientID] = strings.TrimSpace(settings.GitHubOAuthClientID)
- updates[SettingKeyGitHubOAuthRedirectURL] = settings.GitHubOAuthRedirectURL
- updates[SettingKeyGitHubOAuthFrontendRedirectURL] = settings.GitHubOAuthFrontendRedirectURL
- if settings.GitHubOAuthClientSecret != "" {
- updates[SettingKeyGitHubOAuthClientSecret] = strings.TrimSpace(settings.GitHubOAuthClientSecret)
- }
- updates[SettingKeyGoogleOAuthEnabled] = strconv.FormatBool(settings.GoogleOAuthEnabled)
- updates[SettingKeyGoogleOAuthClientID] = strings.TrimSpace(settings.GoogleOAuthClientID)
- updates[SettingKeyGoogleOAuthRedirectURL] = settings.GoogleOAuthRedirectURL
- updates[SettingKeyGoogleOAuthFrontendRedirectURL] = settings.GoogleOAuthFrontendRedirectURL
- if settings.GoogleOAuthClientSecret != "" {
- updates[SettingKeyGoogleOAuthClientSecret] = strings.TrimSpace(settings.GoogleOAuthClientSecret)
- }
- // WeChat Connect OAuth 登录
- updates[SettingKeyWeChatConnectEnabled] = strconv.FormatBool(settings.WeChatConnectEnabled)
- updates[SettingKeyWeChatConnectAppID] = settings.WeChatConnectAppID
- updates[SettingKeyWeChatConnectOpenAppID] = settings.WeChatConnectOpenAppID
- updates[SettingKeyWeChatConnectMPAppID] = settings.WeChatConnectMPAppID
- updates[SettingKeyWeChatConnectMobileAppID] = settings.WeChatConnectMobileAppID
- updates[SettingKeyWeChatConnectOpenEnabled] = strconv.FormatBool(settings.WeChatConnectOpenEnabled)
- updates[SettingKeyWeChatConnectMPEnabled] = strconv.FormatBool(settings.WeChatConnectMPEnabled)
- updates[SettingKeyWeChatConnectMobileEnabled] = strconv.FormatBool(settings.WeChatConnectMobileEnabled)
- updates[SettingKeyWeChatConnectMode] = settings.WeChatConnectMode
- updates[SettingKeyWeChatConnectScopes] = settings.WeChatConnectScopes
- updates[SettingKeyWeChatConnectRedirectURL] = settings.WeChatConnectRedirectURL
- updates[SettingKeyWeChatConnectFrontendRedirectURL] = settings.WeChatConnectFrontendRedirectURL
- if settings.WeChatConnectAppSecret != "" {
- updates[SettingKeyWeChatConnectAppSecret] = settings.WeChatConnectAppSecret
- }
- if settings.WeChatConnectOpenAppSecret != "" {
- updates[SettingKeyWeChatConnectOpenAppSecret] = settings.WeChatConnectOpenAppSecret
- }
- if settings.WeChatConnectMPAppSecret != "" {
- updates[SettingKeyWeChatConnectMPAppSecret] = settings.WeChatConnectMPAppSecret
- }
- if settings.WeChatConnectMobileAppSecret != "" {
- updates[SettingKeyWeChatConnectMobileAppSecret] = settings.WeChatConnectMobileAppSecret
- }
- // OEM设置
- updates[SettingKeySiteName] = settings.SiteName
- updates[SettingKeySiteLogo] = settings.SiteLogo
- updates[SettingKeySiteSubtitle] = settings.SiteSubtitle
- updates[SettingKeyAPIBaseURL] = settings.APIBaseURL
- updates[SettingKeyContactInfo] = settings.ContactInfo
- updates[SettingKeyDocURL] = settings.DocURL
- updates[SettingKeyHomeContent] = settings.HomeContent
- updates[SettingKeyCompactHomeEnabled] = strconv.FormatBool(settings.CompactHomeEnabled)
- updates[SettingKeyHideCcsImportButton] = strconv.FormatBool(settings.HideCcsImportButton)
- updates[SettingKeyPurchaseSubscriptionEnabled] = strconv.FormatBool(settings.PurchaseSubscriptionEnabled)
- updates[SettingKeyPurchaseSubscriptionURL] = strings.TrimSpace(settings.PurchaseSubscriptionURL)
- tableDefaultPageSize, tablePageSizeOptions := normalizeTablePreferences(
- settings.TableDefaultPageSize,
- settings.TablePageSizeOptions,
- )
- updates[SettingKeyTableDefaultPageSize] = strconv.Itoa(tableDefaultPageSize)
- tablePageSizeOptionsJSON, err := json.Marshal(tablePageSizeOptions)
- if err != nil {
- return nil, fmt.Errorf("marshal table page size options: %w", err)
- }
- updates[SettingKeyTablePageSizeOptions] = string(tablePageSizeOptionsJSON)
- updates[SettingKeyCustomMenuItems] = settings.CustomMenuItems
- updates[SettingKeyCustomEndpoints] = settings.CustomEndpoints
- // 默认配置
- updates[SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency)
- updates[SettingKeyDefaultBalance] = strconv.FormatFloat(settings.DefaultBalance, 'f', 8, 64)
- settings.AffiliateRebateRate = clampAffiliateRebateRate(settings.AffiliateRebateRate)
- updates[SettingKeyAffiliateRebateRate] = strconv.FormatFloat(settings.AffiliateRebateRate, 'f', 8, 64)
- if settings.AffiliateRebateFreezeHours < 0 {
- settings.AffiliateRebateFreezeHours = AffiliateRebateFreezeHoursDefault
- }
- if settings.AffiliateRebateFreezeHours > AffiliateRebateFreezeHoursMax {
- settings.AffiliateRebateFreezeHours = AffiliateRebateFreezeHoursMax
- }
- updates[SettingKeyAffiliateRebateFreezeHours] = strconv.Itoa(settings.AffiliateRebateFreezeHours)
- if settings.AffiliateRebateDurationDays < 0 {
- settings.AffiliateRebateDurationDays = AffiliateRebateDurationDaysDefault
- }
- if settings.AffiliateRebateDurationDays > AffiliateRebateDurationDaysMax {
- settings.AffiliateRebateDurationDays = AffiliateRebateDurationDaysMax
- }
- updates[SettingKeyAffiliateRebateDurationDays] = strconv.Itoa(settings.AffiliateRebateDurationDays)
- if settings.AffiliateRebatePerInviteeCap < 0 {
- settings.AffiliateRebatePerInviteeCap = AffiliateRebatePerInviteeCapDefault
- }
- updates[SettingKeyAffiliateRebatePerInviteeCap] = strconv.FormatFloat(settings.AffiliateRebatePerInviteeCap, 'f', 8, 64)
- updates[SettingKeyAffiliateAdminRechargeEnabled] = strconv.FormatBool(settings.AdminRechargeRebateEnabled)
- updates[SettingKeyDefaultUserRPMLimit] = strconv.Itoa(settings.DefaultUserRPMLimit)
- defaultSubsJSON, err := json.Marshal(settings.DefaultSubscriptions)
- if err != nil {
- return nil, fmt.Errorf("marshal default subscriptions: %w", err)
- }
- updates[SettingKeyDefaultSubscriptions] = string(defaultSubsJSON)
- // Model fallback configuration
- updates[SettingKeyEnableModelFallback] = strconv.FormatBool(settings.EnableModelFallback)
- updates[SettingKeyFallbackModelAnthropic] = settings.FallbackModelAnthropic
- updates[SettingKeyFallbackModelOpenAI] = settings.FallbackModelOpenAI
- updates[SettingKeyFallbackModelGemini] = settings.FallbackModelGemini
- updates[SettingKeyFallbackModelAntigravity] = settings.FallbackModelAntigravity
- // Identity patch configuration (Claude -> Gemini)
- updates[SettingKeyEnableIdentityPatch] = strconv.FormatBool(settings.EnableIdentityPatch)
- updates[SettingKeyIdentityPatchPrompt] = settings.IdentityPatchPrompt
- // Ops monitoring (vNext)
- updates[SettingKeyOpsMonitoringEnabled] = strconv.FormatBool(settings.OpsMonitoringEnabled)
- updates[SettingKeyOpsRealtimeMonitoringEnabled] = strconv.FormatBool(settings.OpsRealtimeMonitoringEnabled)
- updates[SettingKeyOpsQueryModeDefault] = string(ParseOpsQueryMode(settings.OpsQueryModeDefault))
- if settings.OpsMetricsIntervalSeconds > 0 {
- updates[SettingKeyOpsMetricsIntervalSeconds] = strconv.Itoa(settings.OpsMetricsIntervalSeconds)
- }
- // Channel monitor feature switch
- updates[SettingKeyChannelMonitorEnabled] = strconv.FormatBool(settings.ChannelMonitorEnabled)
- updates[SettingKeyChannelMonitorMode] = normalizeChannelMonitorMode(settings.ChannelMonitorMode)
- if v := clampChannelMonitorInterval(settings.ChannelMonitorDefaultIntervalSeconds); v > 0 {
- updates[SettingKeyChannelMonitorDefaultIntervalSeconds] = strconv.Itoa(v)
- }
- updates[SettingKeyChannelMonitorHideThroughput] = strconv.FormatBool(settings.ChannelMonitorHideThroughput)
- updates[SettingKeyChannelMonitorShowQuota] = strconv.FormatBool(settings.ChannelMonitorShowQuota)
- updates[SettingKeyChannelMonitorHideUserRanking] = strconv.FormatBool(settings.ChannelMonitorHideUserRanking)
- // Grok model mapping policy
- if v := strings.TrimSpace(settings.GrokDefaultTextModel); v != "" {
- updates[SettingKeyGrokDefaultTextModel] = v
- } else {
- updates[SettingKeyGrokDefaultTextModel] = "grok-4.6"
- }
- updates[SettingKeyGrokCrossClientModelMapEnabled] = strconv.FormatBool(settings.GrokCrossClientModelMapEnabled)
- updates[SettingKeyGrokDefaultBaseURLMode] = normalizeGrokDefaultBaseURLMode(settings.GrokDefaultBaseURLMode)
- // Available channels feature switch
- updates[SettingKeyAvailableChannelsEnabled] = strconv.FormatBool(settings.AvailableChannelsEnabled)
- // Model plaza feature switches + description
- updates[SettingKeyModelPlazaEnabled] = strconv.FormatBool(settings.ModelPlazaEnabled)
- updates[SettingKeyModelPlazaRequireAuth] = strconv.FormatBool(settings.ModelPlazaRequireAuth)
- updates[SettingKeyModelPlazaDescription] = settings.ModelPlazaDescription
- updates[SettingKeyPluginManagementEnabled] = strconv.FormatBool(settings.PluginManagementEnabled)
- // Affiliate (邀请返利) feature switch
- updates[SettingKeyAffiliateEnabled] = strconv.FormatBool(settings.AffiliateEnabled)
- // 风控中心功能开关
- updates[SettingKeyRiskControlEnabled] = strconv.FormatBool(settings.RiskControlEnabled)
- // cyber 会话屏蔽开关 + TTL
- updates[SettingKeyCyberSessionBlockEnabled] = strconv.FormatBool(settings.CyberSessionBlockEnabled)
- if settings.CyberSessionBlockTTLSeconds > 0 {
- updates[SettingKeyCyberSessionBlockTTLSeconds] = strconv.Itoa(settings.CyberSessionBlockTTLSeconds)
- }
- // Claude Code version check
- updates[SettingKeyMinClaudeCodeVersion] = settings.MinClaudeCodeVersion
- updates[SettingKeyMaxClaudeCodeVersion] = settings.MaxClaudeCodeVersion
- // 分组隔离
- updates[SettingKeyAllowUngroupedKeyScheduling] = strconv.FormatBool(settings.AllowUngroupedKeyScheduling)
- // Backend Mode
- updates[SettingKeyBackendModeEnabled] = strconv.FormatBool(settings.BackendModeEnabled)
- // Gateway forwarding behavior
- mode := normalizeOpenAITTFTMode(settings.OpenAITTFTMode)
- if strings.TrimSpace(settings.OpenAITTFTMode) != "" && strings.ToLower(strings.TrimSpace(settings.OpenAITTFTMode)) != OpenAITTFTModeSemantic && strings.ToLower(strings.TrimSpace(settings.OpenAITTFTMode)) != OpenAITTFTModeVisible {
- return nil, fmt.Errorf("%s must be one of: %s/%s", SettingKeyOpenAITTFTMode, OpenAITTFTModeSemantic, OpenAITTFTModeVisible)
- }
- updates[SettingKeyOpenAITTFTMode] = mode
- updates[SettingKeyEnableFingerprintUnification] = strconv.FormatBool(settings.EnableFingerprintUnification)
- updates[SettingKeyEnableMetadataPassthrough] = strconv.FormatBool(settings.EnableMetadataPassthrough)
- updates[SettingKeyEnableCCHSigning] = strconv.FormatBool(settings.EnableCCHSigning)
- updates[SettingKeyEnableClaudeOAuthSystemPromptInjection] = strconv.FormatBool(settings.EnableClaudeOAuthSystemPromptInjection)
- updates[SettingKeyClaudeOAuthSystemPrompt] = settings.ClaudeOAuthSystemPrompt
- if err := ValidateClaudeOAuthSystemPromptBlocksConfig(settings.ClaudeOAuthSystemPromptBlocks); err != nil {
- return nil, err
- }
- updates[SettingKeyClaudeOAuthSystemPromptBlocks] = settings.ClaudeOAuthSystemPromptBlocks
- updates[SettingKeyEnableAnthropicCacheTTL1hInjection] = strconv.FormatBool(settings.EnableAnthropicCacheTTL1hInjection)
- updates[SettingKeyRewriteMessageCacheControl] = strconv.FormatBool(settings.RewriteMessageCacheControl)
- updates[SettingKeyEnableClientDatelineNormalization] = strconv.FormatBool(settings.EnableClientDatelineNormalization)
- updates[SettingKeyAntigravityUserAgentVersion] = antigravity.NormalizeUserAgentVersion(settings.AntigravityUserAgentVersion)
- updates[SettingKeyOpenAICodexUserAgent] = strings.TrimSpace(settings.OpenAICodexUserAgent)
- updates[SettingKeyOpenAICodexClientVersion] = NormalizeCodexClientVersion(settings.OpenAICodexClientVersion)
- updates[SettingKeyOpenAICodexVersionAutoSyncEnabled] = strconv.FormatBool(settings.OpenAICodexVersionAutoSyncEnabled)
- // SettingKeyOpenAICodexClientVersionSynced 由自动同步任务独占写入,此处不得覆盖,
- // 否则面板保存会把同步结果清空。
- // codex_cli_only 加固
- updates[SettingKeyMinCodexVersion] = strings.TrimSpace(settings.MinCodexVersion)
- updates[SettingKeyMaxCodexVersion] = strings.TrimSpace(settings.MaxCodexVersion)
- updates[SettingKeyCodexCLIOnlyBlacklist] = strings.TrimSpace(settings.CodexCLIOnlyBlacklist)
- updates[SettingKeyCodexCLIOnlyWhitelist] = strings.TrimSpace(settings.CodexCLIOnlyWhitelist)
- updates[SettingKeyCodexCLIOnlyAllowAppServerClients] = strconv.FormatBool(settings.CodexCLIOnlyAllowAppServerClients)
- updates[SettingKeyCodexCLIOnlyEngineFingerprintSignals] = strings.TrimSpace(settings.CodexCLIOnlyEngineFingerprintSignals)
- updates[SettingPaymentVisibleMethodAlipaySource] = settings.PaymentVisibleMethodAlipaySource
- updates[SettingPaymentVisibleMethodWxpaySource] = settings.PaymentVisibleMethodWxpaySource
- updates[SettingPaymentVisibleMethodAlipayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodAlipayEnabled)
- updates[SettingPaymentVisibleMethodWxpayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodWxpayEnabled)
- updates[SettingKeyOpenAILowUpstreamRatePriorityEnabled] = strconv.FormatBool(settings.OpenAILowUpstreamRatePriorityEnabled)
- updates[SettingKeyOpenAIOAuthSchedulingRateMultiplier] = strconv.FormatFloat(settings.OpenAIOAuthSchedulingRateMultiplier, 'f', -1, 64)
- updates[openAIAdvancedSchedulerSettingKey] = strconv.FormatBool(settings.OpenAIAdvancedSchedulerEnabled)
- updates[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled] = strconv.FormatBool(settings.OpenAIAdvancedSchedulerStickyWeightedEnabled)
- updates[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled] = strconv.FormatBool(settings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled)
- updates[SettingKeyOpenAIAdvancedSchedulerLBTopK] = settings.OpenAIAdvancedSchedulerLBTopK
- updates[SettingKeyOpenAIAdvancedSchedulerWeightPriority] = settings.OpenAIAdvancedSchedulerWeightPriority
- updates[SettingKeyOpenAIAdvancedSchedulerWeightLoad] = settings.OpenAIAdvancedSchedulerWeightLoad
- updates[SettingKeyOpenAIAdvancedSchedulerWeightQueue] = settings.OpenAIAdvancedSchedulerWeightQueue
- updates[SettingKeyOpenAIAdvancedSchedulerWeightErrorRate] = settings.OpenAIAdvancedSchedulerWeightErrorRate
- updates[SettingKeyOpenAIAdvancedSchedulerWeightTTFT] = settings.OpenAIAdvancedSchedulerWeightTTFT
- updates[SettingKeyOpenAIAdvancedSchedulerWeightReset] = settings.OpenAIAdvancedSchedulerWeightReset
- updates[SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom] = settings.OpenAIAdvancedSchedulerWeightQuotaHeadroom
- updates[SettingKeyOpenAIAdvancedSchedulerWeightUpstreamCost] = settings.OpenAIAdvancedSchedulerWeightUpstreamCost
- updates[SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse] = settings.OpenAIAdvancedSchedulerWeightPreviousResponse
- updates[SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky] = settings.OpenAIAdvancedSchedulerWeightSessionSticky
- // 余额、订阅到期与账号限额通知
- updates[SettingKeyBalanceLowNotifyEnabled] = strconv.FormatBool(settings.BalanceLowNotifyEnabled)
- updates[SettingKeyBalanceLowNotifyThreshold] = strconv.FormatFloat(settings.BalanceLowNotifyThreshold, 'f', 8, 64)
- updates[SettingKeyBalanceLowNotifyRechargeURL] = settings.BalanceLowNotifyRechargeURL
- updates[SettingKeySubscriptionExpiryNotifyEnabled] = strconv.FormatBool(settings.SubscriptionExpiryNotifyEnabled)
- updates[SettingKeyAccountQuotaNotifyEnabled] = strconv.FormatBool(settings.AccountQuotaNotifyEnabled)
- updates[SettingKeyAccountQuotaNotifyEmails] = MarshalNotifyEmails(settings.AccountQuotaNotifyEmails)
- // 系统全局 platform quota:整体替换语义(null/缺省 = 不限制)。
- if settings.DefaultPlatformQuotas != nil {
- if err := validateDefaultPlatformQuotaMap(settings.DefaultPlatformQuotas); err != nil {
- return nil, err
- }
- blob, err := json.Marshal(settings.DefaultPlatformQuotas)
- if err != nil {
- return nil, fmt.Errorf("marshal default platform quotas: %w", err)
- }
- updates[SettingKeyDefaultPlatformQuotas] = string(blob)
- }
- if settings.AccountSchedulingThresholds != nil {
- normalized, err := validateAndNormalizeAccountSchedulingThresholds(settings.AccountSchedulingThresholds)
- if err != nil {
- return nil, err
- }
- blob, err := json.Marshal(normalized)
- if err != nil {
- return nil, fmt.Errorf("marshal account scheduling thresholds: %w", err)
- }
- updates[SettingKeyAccountSchedulingThresholds] = string(blob)
- }
- updates[SettingKeyAllowUserViewErrorRequests] = strconv.FormatBool(settings.AllowUserViewErrorRequests)
- return updates, nil
- }
- func defaultAccountSchedulingThresholds() map[string]int {
- return map[string]int{
- PlatformOpenAI: 100,
- PlatformAnthropic: 100,
- PlatformGrok: 100,
- }
- }
- func validateAndNormalizeAccountSchedulingThresholds(input map[string]int) (map[string]int, error) {
- normalized := defaultAccountSchedulingThresholds()
- for platform, value := range input {
- allowed := false
- for _, item := range AllowedSchedulingThresholdPlatforms {
- if item == platform {
- allowed = true
- break
- }
- }
- if !allowed {
- return nil, infraerrors.BadRequest("INVALID_ACCOUNT_SCHEDULING_THRESHOLDS", fmt.Sprintf("unknown platform %q", platform))
- }
- if value < 1 || value > 100 {
- return nil, infraerrors.BadRequest("INVALID_ACCOUNT_SCHEDULING_THRESHOLDS", "platform scheduling threshold must be between 1 and 100")
- }
- normalized[platform] = value
- }
- return normalized, nil
- }
- func parseAccountSchedulingThresholdsSetting(raw string) (map[string]int, error) {
- thresholds := defaultAccountSchedulingThresholds()
- raw = strings.TrimSpace(raw)
- if raw == "" {
- return thresholds, nil
- }
- parsed := map[string]int{}
- if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
- return thresholds, err
- }
- for _, platform := range AllowedSchedulingThresholdPlatforms {
- if value, ok := parsed[platform]; ok {
- thresholds[platform] = boundedIntOrDefault(value, 1, 100, 100)
- }
- }
- return thresholds, nil
- }
- func boundedIntOrDefault(value, minValue, maxValue, defaultValue int) int {
- if value < minValue || value > maxValue {
- return defaultValue
- }
- return value
- }
- func cloneAccountSchedulingThresholds(input map[string]int) map[string]int {
- if len(input) == 0 {
- return defaultAccountSchedulingThresholds()
- }
- cloned := make(map[string]int, len(input))
- for key, value := range input {
- cloned[key] = value
- }
- return cloned
- }
- // validateDefaultPlatformQuotaMap 校验 platform quota map 的合法性:
- // 平台名须在 AllowedQuotaPlatforms 白名单内,每个非 nil 上限须 finite 且 >= 0。
- // 系统层和 auth-source 层共用此 helper。
- func validateDefaultPlatformQuotaMap(m map[string]*DefaultPlatformQuotaSetting) error {
- for platform, pq := range m {
- if !IsAllowedQuotaPlatform(platform) {
- return infraerrors.BadRequest("INVALID_DEFAULT_PLATFORM_QUOTA", fmt.Sprintf("unknown platform %q", platform))
- }
- if pq == nil {
- continue
- }
- for _, v := range []*float64{pq.DailyLimitUSD, pq.WeeklyLimitUSD, pq.MonthlyLimitUSD} {
- if v != nil && (*v < 0 || math.IsNaN(*v) || math.IsInf(*v, 0)) {
- return infraerrors.BadRequest("INVALID_DEFAULT_PLATFORM_QUOTA", "platform quota limit must be a finite non-negative number")
- }
- }
- }
- return nil
- }
- func (s *SettingService) buildAuthSourceDefaultUpdates(ctx context.Context, settings *AuthSourceDefaultSettings) (map[string]string, error) {
- if settings == nil {
- return nil, nil
- }
- for _, subscriptions := range [][]DefaultSubscriptionSetting{
- settings.Email.Subscriptions,
- settings.LinuxDo.Subscriptions,
- settings.OIDC.Subscriptions,
- settings.WeChat.Subscriptions,
- settings.GitHub.Subscriptions,
- settings.Google.Subscriptions,
- settings.DingTalk.Subscriptions,
- } {
- if err := s.validateDefaultSubscriptionGroups(ctx, subscriptions); err != nil {
- return nil, err
- }
- }
- // 校验各 auth source 的 platform quota map(改动 C:对等系统层校验)
- for _, pgs := range []struct {
- name string
- pq map[string]*DefaultPlatformQuotaSetting
- }{
- {"email", settings.Email.PlatformQuotas},
- {"linuxdo", settings.LinuxDo.PlatformQuotas},
- {"oidc", settings.OIDC.PlatformQuotas},
- {"wechat", settings.WeChat.PlatformQuotas},
- {"github", settings.GitHub.PlatformQuotas},
- {"google", settings.Google.PlatformQuotas},
- {"dingtalk", settings.DingTalk.PlatformQuotas},
- } {
- if pgs.pq != nil {
- if err := validateDefaultPlatformQuotaMap(pgs.pq); err != nil {
- return nil, err
- }
- }
- }
- updates := make(map[string]string, 36)
- writeProviderDefaultGrantUpdates(updates, emailAuthSourceDefaultKeys, settings.Email)
- writeProviderDefaultGrantUpdates(updates, linuxDoAuthSourceDefaultKeys, settings.LinuxDo)
- writeProviderDefaultGrantUpdates(updates, oidcAuthSourceDefaultKeys, settings.OIDC)
- writeProviderDefaultGrantUpdates(updates, weChatAuthSourceDefaultKeys, settings.WeChat)
- writeProviderDefaultGrantUpdates(updates, gitHubAuthSourceDefaultKeys, settings.GitHub)
- writeProviderDefaultGrantUpdates(updates, googleAuthSourceDefaultKeys, settings.Google)
- writeProviderDefaultGrantUpdates(updates, dingTalkAuthSourceDefaultKeys, settings.DingTalk)
- updates[SettingKeyForceEmailOnThirdPartySignup] = strconv.FormatBool(settings.ForceEmailOnThirdPartySignup)
- return updates, nil
- }
- func (s *SettingService) refreshCachedSettings(settings *SystemSettings) {
- if settings == nil {
- return
- }
- // 先使 inflight singleflight 失效,再刷新缓存,缩小旧值覆盖新值的竞态窗口
- versionBoundsSF.Forget("version_bounds")
- versionBoundsCache.Store(&cachedVersionBounds{
- min: settings.MinClaudeCodeVersion,
- max: settings.MaxClaudeCodeVersion,
- expiresAt: time.Now().Add(versionBoundsCacheTTL).UnixNano(),
- })
- backendModeSF.Forget("backend_mode")
- backendModeCache.Store(&cachedBackendMode{
- value: settings.BackendModeEnabled,
- expiresAt: time.Now().Add(backendModeCacheTTL).UnixNano(),
- })
- gatewayForwardingSF.Forget("gateway_forwarding")
- gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
- openAITTFTMode: normalizeOpenAITTFTMode(settings.OpenAITTFTMode),
- fingerprintUnification: settings.EnableFingerprintUnification,
- metadataPassthrough: settings.EnableMetadataPassthrough,
- cchSigning: settings.EnableCCHSigning,
- claudeOAuthSystemPromptInjection: settings.EnableClaudeOAuthSystemPromptInjection,
- claudeOAuthSystemPrompt: settings.ClaudeOAuthSystemPrompt,
- claudeOAuthSystemPromptBlocks: settings.ClaudeOAuthSystemPromptBlocks,
- anthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
- rewriteMessageCacheControl: settings.RewriteMessageCacheControl,
- clientDatelineNormalization: settings.EnableClientDatelineNormalization,
- expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(),
- })
- s.antigravityUAVersionSF.Forget("antigravity_user_agent_version")
- antigravityUserAgentVersion := antigravity.NormalizeUserAgentVersion(settings.AntigravityUserAgentVersion)
- if antigravityUserAgentVersion == "" {
- antigravityUserAgentVersion = antigravity.GetDefaultUserAgentVersion()
- }
- s.antigravityUAVersionCache.Store(&cachedAntigravityUserAgentVersion{
- version: antigravityUserAgentVersion,
- expiresAt: time.Now().Add(antigravityUserAgentVersionCacheTTL).UnixNano(),
- })
- s.openAICodexUASF.Forget("openai_codex_user_agent")
- codexUA := strings.TrimSpace(settings.OpenAICodexUserAgent)
- if codexUA == "" {
- codexUA = DefaultOpenAICodexUserAgent
- }
- s.openAICodexUACache.Store(&cachedOpenAICodexUserAgent{
- value: codexUA,
- expiresAt: time.Now().Add(openAICodexUserAgentCacheTTL).UnixNano(),
- })
- // 版本号缓存只做失效,不在此重算:生效值还取决于自动同步写入的 synced 键,
- // 这里没有它的最新值,重算会把同步结果覆盖成陈旧值。
- s.InvalidateOpenAICodexClientVersionCache()
- openAIAdvancedSchedulerSettingSF.Forget(openAIAdvancedSchedulerSettingKey)
- openAIAdvancedSchedulerSettingCache.Store(&cachedOpenAIAdvancedSchedulerSetting{
- lowUpstreamRatePriorityEnabled: settings.OpenAILowUpstreamRatePriorityEnabled,
- oauthSchedulingRateMultiplier: settings.OpenAIOAuthSchedulingRateMultiplier,
- enabled: settings.OpenAIAdvancedSchedulerEnabled,
- stickyWeightedEnabled: settings.OpenAIAdvancedSchedulerStickyWeightedEnabled,
- subscriptionPriorityEnabled: settings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled,
- lbTopKOverride: parsePositiveIntOverride(settings.OpenAIAdvancedSchedulerLBTopK),
- weightOverrides: parseOpenAIAdvancedSchedulerWeightOverrides(map[string]string{
- SettingKeyOpenAIAdvancedSchedulerWeightPriority: settings.OpenAIAdvancedSchedulerWeightPriority,
- SettingKeyOpenAIAdvancedSchedulerWeightLoad: settings.OpenAIAdvancedSchedulerWeightLoad,
- SettingKeyOpenAIAdvancedSchedulerWeightQueue: settings.OpenAIAdvancedSchedulerWeightQueue,
- SettingKeyOpenAIAdvancedSchedulerWeightErrorRate: settings.OpenAIAdvancedSchedulerWeightErrorRate,
- SettingKeyOpenAIAdvancedSchedulerWeightTTFT: settings.OpenAIAdvancedSchedulerWeightTTFT,
- SettingKeyOpenAIAdvancedSchedulerWeightReset: settings.OpenAIAdvancedSchedulerWeightReset,
- SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom: settings.OpenAIAdvancedSchedulerWeightQuotaHeadroom,
- SettingKeyOpenAIAdvancedSchedulerWeightUpstreamCost: settings.OpenAIAdvancedSchedulerWeightUpstreamCost,
- SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse: settings.OpenAIAdvancedSchedulerWeightPreviousResponse,
- SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky: settings.OpenAIAdvancedSchedulerWeightSessionSticky,
- }),
- expiresAt: time.Now().Add(openAIAdvancedSchedulerSettingCacheTTL).UnixNano(),
- })
- // Invalidate the quota auto-pause cache and let the next read trigger a fresh load.
- // We can't know from here whether ops_advanced_settings was also touched, so be
- // defensive: store an expired entry — GetOpenAIQuotaAutoPauseSettings will serve
- // stale and kick off an async refresh, never blocking the request that follows.
- s.openAIQuotaAutoPauseSettingsSF.Forget(openAIQuotaAutoPauseSettingsRefreshKey)
- if cached, _ := s.openAIQuotaAutoPauseSettingsCache.Load().(*cachedOpenAIQuotaAutoPauseSettings); cached != nil {
- s.openAIQuotaAutoPauseSettingsCache.Store(&cachedOpenAIQuotaAutoPauseSettings{
- settings: cached.settings,
- expiresAt: 0,
- })
- }
- accountSchedulingThresholdsSF.Forget(SettingKeyAccountSchedulingThresholds)
- if settings.AccountSchedulingThresholds != nil {
- normalizedThresholds, err := validateAndNormalizeAccountSchedulingThresholds(settings.AccountSchedulingThresholds)
- if err != nil {
- normalizedThresholds = defaultAccountSchedulingThresholds()
- }
- accountSchedulingThresholdsCache.Store(&cachedAccountSchedulingThresholds{
- thresholds: cloneAccountSchedulingThresholds(normalizedThresholds),
- expiresAt: time.Now().Add(accountSchedulingThresholdsCacheTTL).UnixNano(),
- })
- } else {
- // Partial/omitted payload: clear cache so the next hot-path read reloads from DB.
- accountSchedulingThresholdsCache.Store(&cachedAccountSchedulingThresholds{})
- }
- if s.cfg != nil {
- s.cfg.SetForwardedClientIPSettings(settings.APIKeyACLTrustForwardedIP, settings.ForwardedClientIPHeaders)
- }
- // codex_cli_only 加固策略缓存:设置更新后强制下次重载(涉及 4 个键 + JSON 解析,直接置过期)。
- s.codexRestrictionPolicySF.Forget("codex_restriction_policy")
- s.codexRestrictionPolicyCache.Store(&cachedCodexRestrictionPolicy{expiresAt: 0})
- if s.onUpdate != nil {
- s.onUpdate() // Invalidate cache after settings update
- }
- s.notifyChannelMonitorRuntimeListeners()
- }
- func (s *SettingService) defaultRewriteMessageCacheControl() bool {
- return false
- }
- func (s *SettingService) validateDefaultSubscriptionGroups(ctx context.Context, items []DefaultSubscriptionSetting) error {
- if len(items) == 0 {
- return nil
- }
- checked := make(map[int64]struct{}, len(items))
- for _, item := range items {
- if item.GroupID <= 0 {
- continue
- }
- if _, ok := checked[item.GroupID]; ok {
- return ErrDefaultSubGroupDuplicate.WithMetadata(map[string]string{
- "group_id": strconv.FormatInt(item.GroupID, 10),
- })
- }
- checked[item.GroupID] = struct{}{}
- if s.defaultSubGroupReader == nil {
- continue
- }
- group, err := s.defaultSubGroupReader.GetByID(ctx, item.GroupID)
- if err != nil {
- if errors.Is(err, ErrGroupNotFound) {
- return ErrDefaultSubGroupInvalid.WithMetadata(map[string]string{
- "group_id": strconv.FormatInt(item.GroupID, 10),
- })
- }
- return fmt.Errorf("get default subscription group %d: %w", item.GroupID, err)
- }
- if !group.IsSubscriptionType() {
- return ErrDefaultSubGroupInvalid.WithMetadata(map[string]string{
- "group_id": strconv.FormatInt(item.GroupID, 10),
- })
- }
- }
- return nil
- }
|