2026-07-07 23:35:47 +08:00
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"math"
"sort"
"strconv"
"strings"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
2026-08-07 12:31:14 +08:00
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
2026-07-07 23:35:47 +08:00
)
// InitializeDefaultSettings 初始化默认设置
func ( s * SettingService ) InitializeDefaultSettings ( ctx context . Context ) error {
// 检查是否已有设置
_ , err := s . settingRepo . GetValue ( ctx , SettingKeyRegistrationEnabled )
if err == nil {
// 已有设置,不需要初始化
return nil
}
if ! errors . Is ( err , ErrSettingNotFound ) {
return fmt . Errorf ( "check existing settings: %w" , err )
}
oidcUsePKCEDefault := true
oidcValidateIDTokenDefault := true
if s != nil && s . cfg != nil {
if s . cfg . OIDC . UsePKCEExplicit {
oidcUsePKCEDefault = s . cfg . OIDC . UsePKCE
}
if s . cfg . OIDC . ValidateIDTokenExplicit {
oidcValidateIDTokenDefault = s . cfg . OIDC . ValidateIDToken
}
}
loginAgreementDocumentsJSON , err := marshalLoginAgreementDocuments ( defaultLoginAgreementDocuments ())
if err != nil {
return err
}
2026-07-20 00:09:11 +08:00
forwardedClientIPHeaders := [] string {}
if s != nil && s . cfg != nil {
forwardedClientIPHeaders = s . cfg . ForwardedClientIPSettings (). Headers
}
forwardedClientIPHeadersJSON , err := json . Marshal ( forwardedClientIPHeaders )
if err != nil {
return fmt . Errorf ( "marshal default forwarded client IP headers: %w" , err )
}
2026-07-07 23:35:47 +08:00
// 初始化默认设置
defaults := map [ string ] string {
SettingKeyRegistrationEnabled : "true" ,
SettingKeyEmailVerifyEnabled : "false" ,
SettingKeyRegistrationEmailSuffixWhitelist : "[]" ,
2026-08-09 15:53:40 +08:00
SettingKeyRegistrationEmailDomainQuotaEnabled : "false" ,
2026-07-07 23:35:47 +08:00
SettingKeyPromoCodeEnabled : "true" , // 默认启用优惠码功能
SettingKeyLoginAgreementEnabled : "false" ,
SettingKeyLoginAgreementMode : defaultLoginAgreementMode ,
SettingKeyLoginAgreementUpdatedAt : defaultLoginAgreementDate ,
SettingKeyLoginAgreementDocuments : loginAgreementDocumentsJSON ,
2026-07-19 21:41:45 +08:00
SettingKeyAPIKeyACLTrustForwardedIP : "true" ,
2026-07-20 00:09:11 +08:00
SettingKeyForwardedClientIPHeaders : string ( forwardedClientIPHeadersJSON ),
2026-07-19 21:41:45 +08:00
settingKeyForwardedClientIPModeV2 : "true" ,
2026-07-07 23:35:47 +08:00
SettingKeySiteName : "Sub2API" ,
SettingKeySiteLogo : "" ,
SettingKeyPurchaseSubscriptionEnabled : "false" ,
SettingKeyPurchaseSubscriptionURL : "" ,
SettingKeyTableDefaultPageSize : "20" ,
SettingKeyTablePageSizeOptions : "[10,20,50,100]" ,
SettingKeyCustomMenuItems : "[]" ,
SettingKeyCustomEndpoints : "[]" ,
SettingKeyWeChatConnectEnabled : "false" ,
SettingKeyWeChatConnectAppID : "" ,
SettingKeyWeChatConnectAppSecret : "" ,
SettingKeyWeChatConnectOpenAppID : "" ,
SettingKeyWeChatConnectOpenAppSecret : "" ,
SettingKeyWeChatConnectMPAppID : "" ,
SettingKeyWeChatConnectMPAppSecret : "" ,
SettingKeyWeChatConnectMobileAppID : "" ,
SettingKeyWeChatConnectMobileAppSecret : "" ,
SettingKeyWeChatConnectOpenEnabled : "false" ,
SettingKeyWeChatConnectMPEnabled : "false" ,
SettingKeyWeChatConnectMobileEnabled : "false" ,
SettingKeyWeChatConnectMode : "open" ,
SettingKeyWeChatConnectScopes : "snsapi_login" ,
SettingKeyWeChatConnectRedirectURL : "" ,
SettingKeyWeChatConnectFrontendRedirectURL : defaultWeChatConnectFrontend ,
SettingKeyGitHubOAuthEnabled : "false" ,
SettingKeyGitHubOAuthClientID : "" ,
SettingKeyGitHubOAuthClientSecret : "" ,
SettingKeyGitHubOAuthRedirectURL : "" ,
SettingKeyGitHubOAuthFrontendRedirectURL : defaultGitHubOAuthFrontend ,
SettingKeyGoogleOAuthEnabled : "false" ,
SettingKeyGoogleOAuthClientID : "" ,
SettingKeyGoogleOAuthClientSecret : "" ,
SettingKeyGoogleOAuthRedirectURL : "" ,
SettingKeyGoogleOAuthFrontendRedirectURL : defaultGoogleOAuthFrontend ,
SettingKeyOIDCConnectEnabled : "false" ,
SettingKeyOIDCConnectProviderName : "OIDC" ,
SettingKeyOIDCConnectClientID : "" ,
SettingKeyOIDCConnectClientSecret : "" ,
SettingKeyOIDCConnectIssuerURL : "" ,
SettingKeyOIDCConnectDiscoveryURL : "" ,
SettingKeyOIDCConnectAuthorizeURL : "" ,
SettingKeyOIDCConnectTokenURL : "" ,
SettingKeyOIDCConnectUserInfoURL : "" ,
SettingKeyOIDCConnectJWKSURL : "" ,
SettingKeyOIDCConnectScopes : "openid email profile" ,
SettingKeyOIDCConnectRedirectURL : "" ,
SettingKeyOIDCConnectFrontendRedirectURL : "/auth/oidc/callback" ,
SettingKeyOIDCConnectTokenAuthMethod : "client_secret_post" ,
SettingKeyOIDCConnectUsePKCE : strconv . FormatBool ( oidcUsePKCEDefault ),
SettingKeyOIDCConnectValidateIDToken : strconv . FormatBool ( oidcValidateIDTokenDefault ),
SettingKeyOIDCConnectAllowedSigningAlgs : "RS256,ES256,PS256" ,
SettingKeyOIDCConnectClockSkewSeconds : "120" ,
SettingKeyOIDCConnectRequireEmailVerified : "false" ,
SettingKeyOIDCConnectUserInfoEmailPath : "" ,
SettingKeyOIDCConnectUserInfoIDPath : "" ,
SettingKeyOIDCConnectUserInfoUsernamePath : "" ,
SettingKeyDefaultConcurrency : strconv . Itoa ( s . cfg . Default . UserConcurrency ),
SettingKeyDefaultBalance : strconv . FormatFloat ( s . cfg . Default . UserBalance , 'f' , 8 , 64 ),
SettingKeyAffiliateRebateRate : strconv . FormatFloat ( AffiliateRebateRateDefault , 'f' , 8 , 64 ),
SettingKeyAffiliateRebateFreezeHours : strconv . Itoa ( AffiliateRebateFreezeHoursDefault ),
SettingKeyAffiliateRebateDurationDays : strconv . Itoa ( AffiliateRebateDurationDaysDefault ),
SettingKeyAffiliateRebatePerInviteeCap : strconv . FormatFloat ( AffiliateRebatePerInviteeCapDefault , 'f' , 2 , 64 ),
SettingKeyDefaultUserRPMLimit : "0" ,
SettingKeyDefaultSubscriptions : "[]" ,
SettingKeyAuthSourceDefaultEmailBalance : "0" ,
SettingKeyAuthSourceDefaultEmailConcurrency : "5" ,
SettingKeyAuthSourceDefaultEmailSubscriptions : "[]" ,
SettingKeyAuthSourceDefaultEmailGrantOnSignup : "false" ,
SettingKeyAuthSourceDefaultEmailGrantOnFirstBind : "false" ,
SettingKeyAuthSourceDefaultLinuxDoBalance : "0" ,
SettingKeyAuthSourceDefaultLinuxDoConcurrency : "5" ,
SettingKeyAuthSourceDefaultLinuxDoSubscriptions : "[]" ,
SettingKeyAuthSourceDefaultLinuxDoGrantOnSignup : "false" ,
SettingKeyAuthSourceDefaultLinuxDoGrantOnFirstBind : "false" ,
SettingKeyAuthSourceDefaultOIDCBalance : "0" ,
SettingKeyAuthSourceDefaultOIDCConcurrency : "5" ,
SettingKeyAuthSourceDefaultOIDCSubscriptions : "[]" ,
SettingKeyAuthSourceDefaultOIDCGrantOnSignup : "false" ,
SettingKeyAuthSourceDefaultOIDCGrantOnFirstBind : "false" ,
SettingKeyAuthSourceDefaultWeChatBalance : "0" ,
SettingKeyAuthSourceDefaultWeChatConcurrency : "5" ,
SettingKeyAuthSourceDefaultWeChatSubscriptions : "[]" ,
SettingKeyAuthSourceDefaultWeChatGrantOnSignup : "false" ,
SettingKeyAuthSourceDefaultWeChatGrantOnFirstBind : "false" ,
SettingKeyAuthSourceDefaultGitHubBalance : "0" ,
SettingKeyAuthSourceDefaultGitHubConcurrency : "5" ,
SettingKeyAuthSourceDefaultGitHubSubscriptions : "[]" ,
SettingKeyAuthSourceDefaultGitHubGrantOnSignup : "false" ,
SettingKeyAuthSourceDefaultGitHubGrantOnFirstBind : "false" ,
SettingKeyAuthSourceDefaultGoogleBalance : "0" ,
SettingKeyAuthSourceDefaultGoogleConcurrency : "5" ,
SettingKeyAuthSourceDefaultGoogleSubscriptions : "[]" ,
SettingKeyAuthSourceDefaultGoogleGrantOnSignup : "false" ,
SettingKeyAuthSourceDefaultGoogleGrantOnFirstBind : "false" ,
SettingKeyAuthSourceDefaultDingTalkBalance : "0" ,
SettingKeyAuthSourceDefaultDingTalkConcurrency : "5" ,
SettingKeyAuthSourceDefaultDingTalkSubscriptions : "[]" ,
SettingKeyAuthSourceDefaultDingTalkGrantOnSignup : "false" ,
SettingKeyAuthSourceDefaultDingTalkGrantOnFirstBind : "false" ,
SettingKeyForceEmailOnThirdPartySignup : "false" ,
SettingKeySMTPPort : "587" ,
SettingKeySMTPUseTLS : "false" ,
// Model fallback defaults
SettingKeyEnableModelFallback : "false" ,
SettingKeyFallbackModelAnthropic : "claude-3-5-sonnet-20241022" ,
SettingKeyFallbackModelOpenAI : "gpt-4o" ,
SettingKeyFallbackModelGemini : "gemini-2.5-pro" ,
SettingKeyFallbackModelAntigravity : "gemini-2.5-pro" ,
// Identity patch defaults
SettingKeyEnableIdentityPatch : "true" ,
SettingKeyIdentityPatchPrompt : "" ,
// Ops monitoring defaults (vNext)
SettingKeyOpsMonitoringEnabled : "true" ,
SettingKeyOpsRealtimeMonitoringEnabled : "true" ,
SettingKeyOpsQueryModeDefault : "auto" ,
SettingKeyOpsMetricsIntervalSeconds : "60" ,
// Channel monitor defaults (enabled, 60s)
SettingKeyChannelMonitorEnabled : "true" ,
2026-08-08 13:56:22 +08:00
SettingKeyChannelMonitorMode : ChannelMonitorModeV1 ,
2026-07-07 23:35:47 +08:00
SettingKeyChannelMonitorDefaultIntervalSeconds : "60" ,
2026-08-08 12:49:39 +08:00
SettingKeyChannelMonitorHideThroughput : "true" ,
2026-08-18 04:06:18 +00:00
SettingKeyChannelMonitorShowQuota : "false" ,
2026-09-07 18:31:03 +08:00
SettingKeyChannelMonitorHideUserRanking : "false" ,
2026-07-07 23:35:47 +08:00
2026-09-05 23:17:34 +08:00
// Grok compatibility defaults: cross-client mapping stays enabled unless
// operators explicitly disable it.
2026-08-20 02:32:22 -07:00
SettingKeyGrokDefaultTextModel : "grok-4.6" ,
2026-08-08 14:39:22 +08:00
SettingKeyGrokCrossClientModelMapEnabled : "true" ,
2026-08-08 01:07:19 +08:00
SettingKeyGrokDefaultBaseURLMode : GrokDefaultBaseURLModeCLI ,
2026-08-07 12:31:14 +08:00
2026-07-07 23:35:47 +08:00
// Available channels feature (default disabled; opt-in)
SettingKeyAvailableChannelsEnabled : "false" ,
2026-09-10 23:27:28 +08:00
// Subscription feature (default enabled; opt-out)
SettingKeySubscriptionEnabled : "true" ,
2026-07-28 15:55:56 +08:00
// Model plaza feature (default disabled; opt-in, public unless require_auth)
2026-08-24 09:03:37 +08:00
SettingKeyModelPlazaEnabled : "false" ,
SettingKeyModelPlazaRequireAuth : "false" ,
SettingKeyModelPlazaDescription : "" ,
SettingKeyPluginManagementEnabled : "false" ,
2026-07-28 15:55:56 +08:00
2026-07-07 23:35:47 +08:00
// Affiliate (邀请返利) feature (default disabled; opt-in)
2026-07-15 04:28:55 -05:00
SettingKeyAffiliateEnabled : "false" ,
SettingKeyAffiliateAdminRechargeEnabled : strconv . FormatBool ( AdminRechargeRebateEnabledDefault ),
2026-07-07 23:35:47 +08:00
// 风控中心功能(默认关闭,显式启用)
SettingKeyRiskControlEnabled : "false" ,
// cyber 会话屏蔽(默认关闭,TTL 默认 3600s)
SettingKeyCyberSessionBlockEnabled : "false" ,
SettingKeyCyberSessionBlockTTLSeconds : "3600" ,
// Claude Code version check (default: empty = disabled)
SettingKeyMinClaudeCodeVersion : "" ,
SettingKeyMaxClaudeCodeVersion : "" ,
// codex_cli_only 加固(默认:版本不检查、名单空、默认种子指纹信号)
SettingKeyMinCodexVersion : "" ,
SettingKeyMaxCodexVersion : "" ,
SettingKeyCodexCLIOnlyBlacklist : "" ,
SettingKeyCodexCLIOnlyWhitelist : "" ,
SettingKeyCodexCLIOnlyAllowAppServerClients : "false" ,
SettingKeyCodexCLIOnlyEngineFingerprintSignals : openai . DefaultEngineFingerprintSignalsJSON (),
// 分组隔离(默认不允许未分组 Key 调度)
SettingKeyAllowUngroupedKeyScheduling : "false" ,
2026-07-15 22:42:54 +08:00
SettingKeyOpenAILowUpstreamRatePriorityEnabled : "false" ,
SettingKeyOpenAIOAuthSchedulingRateMultiplier : "1" ,
2026-07-07 23:35:47 +08:00
SettingKeyEnableAnthropicCacheTTL1hInjection : "false" ,
SettingKeyRewriteMessageCacheControl : strconv . FormatBool ( s . defaultRewriteMessageCacheControl ()),
SettingKeyEnableClientDatelineNormalization : "true" ,
SettingKeyAntigravityUserAgentVersion : "" ,
SettingKeyOpenAICodexUserAgent : "" ,
2026-08-03 20:14:58 +08:00
SettingKeyOpenAICodexClientVersion : "" ,
SettingKeyOpenAICodexClientVersionSynced : "" ,
SettingKeyOpenAICodexVersionAutoSyncEnabled : "true" ,
2026-07-07 23:35:47 +08:00
SettingPaymentVisibleMethodAlipaySource : "" ,
SettingPaymentVisibleMethodWxpaySource : "" ,
SettingPaymentVisibleMethodAlipayEnabled : "false" ,
SettingPaymentVisibleMethodWxpayEnabled : "false" ,
openAIAdvancedSchedulerSettingKey : "false" ,
SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled : "false" ,
SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled : "false" ,
SettingKeyOpenAIAdvancedSchedulerLBTopK : "" ,
SettingKeyOpenAIAdvancedSchedulerWeightPriority : "" ,
SettingKeyOpenAIAdvancedSchedulerWeightLoad : "" ,
SettingKeyOpenAIAdvancedSchedulerWeightQueue : "" ,
SettingKeyOpenAIAdvancedSchedulerWeightErrorRate : "" ,
SettingKeyOpenAIAdvancedSchedulerWeightTTFT : "" ,
SettingKeyOpenAIAdvancedSchedulerWeightReset : "" ,
SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom : "" ,
2026-07-15 22:42:54 +08:00
SettingKeyOpenAIAdvancedSchedulerWeightUpstreamCost : "" ,
2026-07-07 23:35:47 +08:00
SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse : "" ,
SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky : "" ,
SettingKeyAllowUserViewErrorRequests : "false" ,
}
return s . settingRepo . SetMultiple ( ctx , defaults )
}
2026-07-20 00:09:11 +08:00
func parseForwardedClientIPHeadersSetting ( value string ) ([] string , error ) {
var headers [] string
if err := json . Unmarshal ([] byte ( value ), & headers ); err != nil {
return nil , fmt . Errorf ( "parse forwarded_client_ip_headers: %w" , err )
}
if headers == nil {
return nil , fmt . Errorf ( "parse forwarded_client_ip_headers: value must be a JSON array" )
}
normalized , err := config . NormalizeForwardedClientIPHeaders ( headers )
if err != nil {
return nil , fmt . Errorf ( "parse forwarded_client_ip_headers: %w" , err )
}
return normalized , nil
}
2026-07-07 23:35:47 +08:00
// parseSettings 解析设置到结构体
func ( s * SettingService ) parseSettings ( settings map [ string ] string ) * SystemSettings {
emailVerifyEnabled := settings [ SettingKeyEmailVerifyEnabled ] == "true"
loginAgreementDocuments := parseLoginAgreementDocuments ( settings [ SettingKeyLoginAgreementDocuments ])
loginAgreementUpdatedAt := strings . TrimSpace ( settings [ SettingKeyLoginAgreementUpdatedAt ])
if loginAgreementUpdatedAt == "" {
loginAgreementUpdatedAt = defaultLoginAgreementDate
}
apiKeyACLTrustForwardedIP := false
2026-07-20 00:09:11 +08:00
forwardedClientIPHeaders := [] string {}
if s != nil && s . cfg != nil {
runtimeSettings := s . cfg . ForwardedClientIPSettings ()
apiKeyACLTrustForwardedIP = runtimeSettings . TrustForwardedIP
forwardedClientIPHeaders = runtimeSettings . Headers
}
2026-07-07 23:35:47 +08:00
if value , ok := settings [ SettingKeyAPIKeyACLTrustForwardedIP ]; ok {
apiKeyACLTrustForwardedIP = value == "true"
2026-07-20 00:09:11 +08:00
}
if value , ok := settings [ SettingKeyForwardedClientIPHeaders ]; ok {
parsed , err := parseForwardedClientIPHeadersSetting ( value )
if err != nil {
slog . Error ( "invalid persisted forwarded client IP headers; forwarded trust disabled" , "error" , err )
apiKeyACLTrustForwardedIP = false
forwardedClientIPHeaders = [] string {}
} else {
forwardedClientIPHeaders = parsed
}
2026-07-07 23:35:47 +08:00
}
result := & SystemSettings {
2026-08-04 15:09:29 +08:00
RegistrationEnabled : settings [ SettingKeyRegistrationEnabled ] == "true" ,
EmailVerifyEnabled : emailVerifyEnabled ,
RegistrationEmailSuffixWhitelist : ParseRegistrationEmailSuffixWhitelist ( settings [ SettingKeyRegistrationEmailSuffixWhitelist ]),
2026-08-09 15:53:40 +08:00
RegistrationEmailDomainQuotaEnabled : settings [ SettingKeyRegistrationEmailDomainQuotaEnabled ] == "true" ,
2026-08-04 15:09:29 +08:00
PromoCodeEnabled : settings [ SettingKeyPromoCodeEnabled ] != "false" , // 默认启用
PasswordResetEnabled : emailVerifyEnabled && settings [ SettingKeyPasswordResetEnabled ] == "true" ,
FrontendURL : settings [ SettingKeyFrontendURL ],
InvitationCodeEnabled : settings [ SettingKeyInvitationCodeEnabled ] == "true" ,
TotpEnabled : settings [ SettingKeyTotpEnabled ] == "true" ,
PasskeyEnabled : s . passkeySettingEnabled ( settings ),
SessionBindingEnabled : settings [ SettingKeySessionBindingEnabled ] == "true" , // 默认关闭
StepUpEnabled : settings [ SettingKeyStepUpEnabled ] == "true" , // 默认关闭
AuditLogRetentionDays : parseAuditLogRetentionDays ( settings [ SettingKeyAuditLogRetentionDays ]),
LoginAgreementEnabled : settings [ SettingKeyLoginAgreementEnabled ] == "true" ,
LoginAgreementMode : normalizeLoginAgreementMode ( settings [ SettingKeyLoginAgreementMode ]),
LoginAgreementUpdatedAt : loginAgreementUpdatedAt ,
LoginAgreementDocuments : loginAgreementDocuments ,
SMTPHost : settings [ SettingKeySMTPHost ],
SMTPUsername : settings [ SettingKeySMTPUsername ],
SMTPFrom : settings [ SettingKeySMTPFrom ],
SMTPFromName : settings [ SettingKeySMTPFromName ],
SMTPUseTLS : settings [ SettingKeySMTPUseTLS ] == "true" ,
SMTPPasswordConfigured : settings [ SettingKeySMTPPassword ] != "" ,
TurnstileEnabled : settings [ SettingKeyTurnstileEnabled ] == "true" ,
TurnstileSiteKey : settings [ SettingKeyTurnstileSiteKey ],
TurnstileSecretKeyConfigured : settings [ SettingKeyTurnstileSecretKey ] != "" ,
TencentCaptchaEnabled : settings [ SettingKeyTencentCaptchaEnabled ] == "true" ,
TencentCaptchaAppID : settings [ SettingKeyTencentCaptchaAppID ],
TencentCaptchaAppSecretKeyConfigured : settings [ SettingKeyTencentCaptchaAppSecretKey ] != "" ,
TencentCaptchaCloudSecretIDConfigured : settings [ SettingKeyTencentCaptchaCloudSecretID ] != "" ,
TencentCaptchaCloudSecretKeyConfigured : settings [ SettingKeyTencentCaptchaCloudSecretKey ] != "" ,
2026-08-06 20:34:37 +08:00
TencentCaptchaRegion : normalizeTencentCaptchaRegion ( settings [ SettingKeyTencentCaptchaRegion ]),
2026-08-03 13:38:44 +08:00
AliyunCaptchaEnabled : settings [ SettingKeyAliyunCaptchaEnabled ] == "true" ,
AliyunCaptchaAccessKeyID : settings [ SettingKeyAliyunCaptchaAccessKeyID ],
AliyunCaptchaAccessKeySecretConfigured : settings [ SettingKeyAliyunCaptchaAccessKeySecret ] != "" ,
AliyunCaptchaSceneID : settings [ SettingKeyAliyunCaptchaSceneID ],
AliyunCaptchaPrefix : settings [ SettingKeyAliyunCaptchaPrefix ],
AliyunCaptchaRegion : normalizeAliyunCaptchaRegion ( settings [ SettingKeyAliyunCaptchaRegion ]),
2026-08-04 15:09:29 +08:00
APIKeyACLTrustForwardedIP : apiKeyACLTrustForwardedIP ,
ForwardedClientIPHeaders : forwardedClientIPHeaders ,
SiteName : s . getStringOrDefault ( settings , SettingKeySiteName , "Sub2API" ),
SiteLogo : settings [ SettingKeySiteLogo ],
SiteSubtitle : s . getStringOrDefault ( settings , SettingKeySiteSubtitle , "Subscription to API Conversion Platform" ),
APIBaseURL : settings [ SettingKeyAPIBaseURL ],
ContactInfo : settings [ SettingKeyContactInfo ],
DocURL : settings [ SettingKeyDocURL ],
HomeContent : settings [ SettingKeyHomeContent ],
CompactHomeEnabled : settings [ SettingKeyCompactHomeEnabled ] == "true" ,
HideCcsImportButton : settings [ SettingKeyHideCcsImportButton ] == "true" ,
PurchaseSubscriptionEnabled : settings [ SettingKeyPurchaseSubscriptionEnabled ] == "true" ,
PurchaseSubscriptionURL : strings . TrimSpace ( settings [ SettingKeyPurchaseSubscriptionURL ]),
CustomMenuItems : settings [ SettingKeyCustomMenuItems ],
CustomEndpoints : settings [ SettingKeyCustomEndpoints ],
BackendModeEnabled : settings [ SettingKeyBackendModeEnabled ] == "true" ,
2026-07-07 23:35:47 +08:00
}
result . TableDefaultPageSize , result . TablePageSizeOptions = parseTablePreferences (
settings [ SettingKeyTableDefaultPageSize ],
settings [ SettingKeyTablePageSizeOptions ],
)
// 解析整数类型
if port , err := strconv . Atoi ( settings [ SettingKeySMTPPort ]); err == nil {
result . SMTPPort = port
} else {
result . SMTPPort = 587
}
if concurrency , err := strconv . Atoi ( settings [ SettingKeyDefaultConcurrency ]); err == nil {
result . DefaultConcurrency = concurrency
} else {
result . DefaultConcurrency = s . cfg . Default . UserConcurrency
}
if rpm , err := strconv . Atoi ( settings [ SettingKeyDefaultUserRPMLimit ]); err == nil && rpm >= 0 {
result . DefaultUserRPMLimit = rpm
}
// 解析浮点数类型
if balance , err := strconv . ParseFloat ( settings [ SettingKeyDefaultBalance ], 64 ); err == nil {
result . DefaultBalance = balance
} else {
result . DefaultBalance = s . cfg . Default . UserBalance
}
if rebateRate , err := strconv . ParseFloat ( settings [ SettingKeyAffiliateRebateRate ], 64 ); err == nil {
result . AffiliateRebateRate = clampAffiliateRebateRate ( rebateRate )
} else {
result . AffiliateRebateRate = AffiliateRebateRateDefault
}
if freezeHours , err := strconv . Atoi ( settings [ SettingKeyAffiliateRebateFreezeHours ]); err == nil && freezeHours >= 0 {
if freezeHours > AffiliateRebateFreezeHoursMax {
freezeHours = AffiliateRebateFreezeHoursMax
}
result . AffiliateRebateFreezeHours = freezeHours
}
if durationDays , err := strconv . Atoi ( settings [ SettingKeyAffiliateRebateDurationDays ]); err == nil && durationDays >= 0 {
if durationDays > AffiliateRebateDurationDaysMax {
durationDays = AffiliateRebateDurationDaysMax
}
result . AffiliateRebateDurationDays = durationDays
}
if perInviteeCap , err := strconv . ParseFloat ( settings [ SettingKeyAffiliateRebatePerInviteeCap ], 64 ); err == nil && perInviteeCap >= 0 {
result . AffiliateRebatePerInviteeCap = perInviteeCap
}
2026-07-15 04:28:55 -05:00
result . AdminRechargeRebateEnabled = settings [ SettingKeyAffiliateAdminRechargeEnabled ] == "true"
2026-07-07 23:35:47 +08:00
result . DefaultSubscriptions = parseDefaultSubscriptions ( settings [ SettingKeyDefaultSubscriptions ])
// 敏感信息直接返回,方便测试连接时使用
result . SMTPPassword = settings [ SettingKeySMTPPassword ]
result . TurnstileSecretKey = settings [ SettingKeyTurnstileSecretKey ]
2026-08-04 15:09:29 +08:00
result . TencentCaptchaAppSecretKey = settings [ SettingKeyTencentCaptchaAppSecretKey ]
result . TencentCaptchaCloudSecretID = settings [ SettingKeyTencentCaptchaCloudSecretID ]
result . TencentCaptchaCloudSecretKey = settings [ SettingKeyTencentCaptchaCloudSecretKey ]
2026-08-03 13:38:44 +08:00
result . AliyunCaptchaAccessKeySecret = settings [ SettingKeyAliyunCaptchaAccessKeySecret ]
2026-07-07 23:35:47 +08:00
// LinuxDo Connect 设置:
// - 兼容 config.yaml/env(避免老部署因为未迁移到数据库设置而被意外关闭)
// - 支持在后台“系统设置”中覆盖并持久化(存储于 DB)
linuxDoBase := config . LinuxDoConnectConfig {}
if s . cfg != nil {
linuxDoBase = s . cfg . LinuxDo
}
if raw , ok := settings [ SettingKeyLinuxDoConnectEnabled ]; ok {
result . LinuxDoConnectEnabled = raw == "true"
} else {
result . LinuxDoConnectEnabled = linuxDoBase . Enabled
}
if v , ok := settings [ SettingKeyLinuxDoConnectClientID ]; ok && strings . TrimSpace ( v ) != "" {
result . LinuxDoConnectClientID = strings . TrimSpace ( v )
} else {
result . LinuxDoConnectClientID = linuxDoBase . ClientID
}
if v , ok := settings [ SettingKeyLinuxDoConnectRedirectURL ]; ok && strings . TrimSpace ( v ) != "" {
result . LinuxDoConnectRedirectURL = strings . TrimSpace ( v )
} else {
result . LinuxDoConnectRedirectURL = linuxDoBase . RedirectURL
}
result . LinuxDoConnectClientSecret = strings . TrimSpace ( settings [ SettingKeyLinuxDoConnectClientSecret ])
if result . LinuxDoConnectClientSecret == "" {
result . LinuxDoConnectClientSecret = strings . TrimSpace ( linuxDoBase . ClientSecret )
}
result . LinuxDoConnectClientSecretConfigured = result . LinuxDoConnectClientSecret != ""
// DingTalk Connect 设置:
// - 兼容 config.yaml/env
// - 支持后台系统设置覆盖并持久化(存储于 DB)
dingTalkBase := config . DingTalkConnectConfig {}
if s . cfg != nil {
dingTalkBase = s . cfg . DingTalk
}
if raw , ok := settings [ SettingKeyDingTalkConnectEnabled ]; ok {
result . DingTalkConnectEnabled = raw == "true"
} else {
result . DingTalkConnectEnabled = dingTalkBase . Enabled
}
if v , ok := settings [ SettingKeyDingTalkConnectClientID ]; ok && strings . TrimSpace ( v ) != "" {
result . DingTalkConnectClientID = strings . TrimSpace ( v )
} else {
result . DingTalkConnectClientID = dingTalkBase . ClientID
}
if v , ok := settings [ SettingKeyDingTalkConnectRedirectURL ]; ok && strings . TrimSpace ( v ) != "" {
result . DingTalkConnectRedirectURL = strings . TrimSpace ( v )
} else {
result . DingTalkConnectRedirectURL = dingTalkBase . RedirectURL
}
result . DingTalkConnectClientSecret = strings . TrimSpace ( settings [ SettingKeyDingTalkConnectClientSecret ])
if result . DingTalkConnectClientSecret == "" {
result . DingTalkConnectClientSecret = strings . TrimSpace ( dingTalkBase . ClientSecret )
}
result . DingTalkConnectClientSecretConfigured = result . DingTalkConnectClientSecret != ""
if v , ok := settings [ SettingKeyDingTalkConnectCorpRestrictionPolicy ]; ok && strings . TrimSpace ( v ) != "" {
result . DingTalkConnectCorpRestrictionPolicy = strings . TrimSpace ( v )
} else {
result . DingTalkConnectCorpRestrictionPolicy = dingTalkBase . CorpRestrictionPolicy
}
result . DingTalkConnectCorpRestrictionPolicy = coerceDeprecatedDingTalkCorpPolicy ( result . DingTalkConnectCorpRestrictionPolicy )
if v , ok := settings [ SettingKeyDingTalkConnectInternalCorpID ]; ok && strings . TrimSpace ( v ) != "" {
result . DingTalkConnectInternalCorpID = strings . TrimSpace ( v )
} else {
result . DingTalkConnectInternalCorpID = dingTalkBase . InternalCorpID
}
if v , ok := settings [ SettingKeyDingTalkConnectBypassRegistration ]; ok && strings . TrimSpace ( v ) != "" {
result . DingTalkConnectBypassRegistration = strings . EqualFold ( strings . TrimSpace ( v ), "true" )
} else {
result . DingTalkConnectBypassRegistration = dingTalkBase . BypassRegistration
}
// bypass_registration 仅在 internal_only 模式下有意义;其它策略下强制 false,
// 以保证加载出的 effective config 永远是一致状态。
if result . DingTalkConnectCorpRestrictionPolicy != "internal_only" {
result . DingTalkConnectBypassRegistration = false
}
if v , ok := settings [ SettingKeyDingTalkConnectSyncCorpEmail ]; ok && strings . TrimSpace ( v ) != "" {
result . DingTalkConnectSyncCorpEmail = strings . EqualFold ( strings . TrimSpace ( v ), "true" )
} else {
result . DingTalkConnectSyncCorpEmail = dingTalkBase . SyncCorpEmail
}
if v , ok := settings [ SettingKeyDingTalkConnectSyncDisplayName ]; ok && strings . TrimSpace ( v ) != "" {
result . DingTalkConnectSyncDisplayName = strings . EqualFold ( strings . TrimSpace ( v ), "true" )
} else {
result . DingTalkConnectSyncDisplayName = dingTalkBase . SyncDisplayName
}
if v , ok := settings [ SettingKeyDingTalkConnectSyncDept ]; ok && strings . TrimSpace ( v ) != "" {
result . DingTalkConnectSyncDept = strings . EqualFold ( strings . TrimSpace ( v ), "true" )
} else {
result . DingTalkConnectSyncDept = dingTalkBase . SyncDept
}
// 身份同步三开关仅在 internal_only 模式下有意义;其它策略强制 false。
if result . DingTalkConnectCorpRestrictionPolicy != "internal_only" {
result . DingTalkConnectSyncCorpEmail = false
result . DingTalkConnectSyncDisplayName = false
result . DingTalkConnectSyncDept = false
}
// 身份同步目标 attr key( DB 空 → fallback 默认值)
result . DingTalkConnectSyncCorpEmailAttrKey = strings . TrimSpace ( settings [ SettingKeyDingTalkConnectSyncCorpEmailAttrKey ])
if result . DingTalkConnectSyncCorpEmailAttrKey == "" {
if v := strings . TrimSpace ( dingTalkBase . SyncCorpEmailAttrKey ); v != "" {
result . DingTalkConnectSyncCorpEmailAttrKey = v
} else {
result . DingTalkConnectSyncCorpEmailAttrKey = "dingtalk_email"
}
}
result . DingTalkConnectSyncDisplayNameAttrKey = strings . TrimSpace ( settings [ SettingKeyDingTalkConnectSyncDisplayNameAttrKey ])
if result . DingTalkConnectSyncDisplayNameAttrKey == "" {
if v := strings . TrimSpace ( dingTalkBase . SyncDisplayNameAttrKey ); v != "" {
result . DingTalkConnectSyncDisplayNameAttrKey = v
} else {
result . DingTalkConnectSyncDisplayNameAttrKey = "dingtalk_name"
}
}
result . DingTalkConnectSyncDeptAttrKey = strings . TrimSpace ( settings [ SettingKeyDingTalkConnectSyncDeptAttrKey ])
if result . DingTalkConnectSyncDeptAttrKey == "" {
if v := strings . TrimSpace ( dingTalkBase . SyncDeptAttrKey ); v != "" {
result . DingTalkConnectSyncDeptAttrKey = v
} else {
result . DingTalkConnectSyncDeptAttrKey = "dingtalk_department"
}
}
// 身份同步目标 attr 显示名称(DB 空 → fallback 默认中文)
result . DingTalkConnectSyncCorpEmailAttrName = strings . TrimSpace ( settings [ SettingKeyDingTalkConnectSyncCorpEmailAttrName ])
if result . DingTalkConnectSyncCorpEmailAttrName == "" {
if v := strings . TrimSpace ( dingTalkBase . SyncCorpEmailAttrName ); v != "" {
result . DingTalkConnectSyncCorpEmailAttrName = v
} else {
result . DingTalkConnectSyncCorpEmailAttrName = "钉钉企业邮箱"
}
}
result . DingTalkConnectSyncDisplayNameAttrName = strings . TrimSpace ( settings [ SettingKeyDingTalkConnectSyncDisplayNameAttrName ])
if result . DingTalkConnectSyncDisplayNameAttrName == "" {
if v := strings . TrimSpace ( dingTalkBase . SyncDisplayNameAttrName ); v != "" {
result . DingTalkConnectSyncDisplayNameAttrName = v
} else {
result . DingTalkConnectSyncDisplayNameAttrName = "钉钉姓名"
}
}
result . DingTalkConnectSyncDeptAttrName = strings . TrimSpace ( settings [ SettingKeyDingTalkConnectSyncDeptAttrName ])
if result . DingTalkConnectSyncDeptAttrName == "" {
if v := strings . TrimSpace ( dingTalkBase . SyncDeptAttrName ); v != "" {
result . DingTalkConnectSyncDeptAttrName = v
} else {
result . DingTalkConnectSyncDeptAttrName = "钉钉部门"
}
}
// Generic OIDC 设置:
// - 兼容 config.yaml/env
// - 支持后台系统设置覆盖并持久化(存储于 DB)
oidcBase := config . OIDCConnectConfig {}
if s . cfg != nil {
oidcBase = s . cfg . OIDC
}
if raw , ok := settings [ SettingKeyOIDCConnectEnabled ]; ok {
result . OIDCConnectEnabled = raw == "true"
} else {
result . OIDCConnectEnabled = oidcBase . Enabled
}
if v , ok := settings [ SettingKeyOIDCConnectProviderName ]; ok && strings . TrimSpace ( v ) != "" {
result . OIDCConnectProviderName = strings . TrimSpace ( v )
} else {
result . OIDCConnectProviderName = strings . TrimSpace ( oidcBase . ProviderName )
}
if result . OIDCConnectProviderName == "" {
result . OIDCConnectProviderName = "OIDC"
}
if v , ok := settings [ SettingKeyOIDCConnectClientID ]; ok && strings . TrimSpace ( v ) != "" {
result . OIDCConnectClientID = strings . TrimSpace ( v )
} else {
result . OIDCConnectClientID = strings . TrimSpace ( oidcBase . ClientID )
}
if v , ok := settings [ SettingKeyOIDCConnectIssuerURL ]; ok && strings . TrimSpace ( v ) != "" {
result . OIDCConnectIssuerURL = strings . TrimSpace ( v )
} else {
result . OIDCConnectIssuerURL = strings . TrimSpace ( oidcBase . IssuerURL )
}
if v , ok := settings [ SettingKeyOIDCConnectDiscoveryURL ]; ok && strings . TrimSpace ( v ) != "" {
result . OIDCConnectDiscoveryURL = strings . TrimSpace ( v )
} else {
result . OIDCConnectDiscoveryURL = strings . TrimSpace ( oidcBase . DiscoveryURL )
}
if v , ok := settings [ SettingKeyOIDCConnectAuthorizeURL ]; ok && strings . TrimSpace ( v ) != "" {
result . OIDCConnectAuthorizeURL = strings . TrimSpace ( v )
} else {
result . OIDCConnectAuthorizeURL = strings . TrimSpace ( oidcBase . AuthorizeURL )
}
if v , ok := settings [ SettingKeyOIDCConnectTokenURL ]; ok && strings . TrimSpace ( v ) != "" {
result . OIDCConnectTokenURL = strings . TrimSpace ( v )
} else {
result . OIDCConnectTokenURL = strings . TrimSpace ( oidcBase . TokenURL )
}
if v , ok := settings [ SettingKeyOIDCConnectUserInfoURL ]; ok && strings . TrimSpace ( v ) != "" {
result . OIDCConnectUserInfoURL = strings . TrimSpace ( v )
} else {
result . OIDCConnectUserInfoURL = strings . TrimSpace ( oidcBase . UserInfoURL )
}
if v , ok := settings [ SettingKeyOIDCConnectJWKSURL ]; ok && strings . TrimSpace ( v ) != "" {
result . OIDCConnectJWKSURL = strings . TrimSpace ( v )
} else {
result . OIDCConnectJWKSURL = strings . TrimSpace ( oidcBase . JWKSURL )
}
if v , ok := settings [ SettingKeyOIDCConnectScopes ]; ok && strings . TrimSpace ( v ) != "" {
result . OIDCConnectScopes = strings . TrimSpace ( v )
} else {
result . OIDCConnectScopes = strings . TrimSpace ( oidcBase . Scopes )
}
if v , ok := settings [ SettingKeyOIDCConnectRedirectURL ]; ok && strings . TrimSpace ( v ) != "" {
result . OIDCConnectRedirectURL = strings . TrimSpace ( v )
} else {
result . OIDCConnectRedirectURL = strings . TrimSpace ( oidcBase . RedirectURL )
}
if v , ok := settings [ SettingKeyOIDCConnectFrontendRedirectURL ]; ok && strings . TrimSpace ( v ) != "" {
result . OIDCConnectFrontendRedirectURL = strings . TrimSpace ( v )
} else {
result . OIDCConnectFrontendRedirectURL = strings . TrimSpace ( oidcBase . FrontendRedirectURL )
}
if v , ok := settings [ SettingKeyOIDCConnectTokenAuthMethod ]; ok && strings . TrimSpace ( v ) != "" {
result . OIDCConnectTokenAuthMethod = strings . ToLower ( strings . TrimSpace ( v ))
} else {
result . OIDCConnectTokenAuthMethod = strings . ToLower ( strings . TrimSpace ( oidcBase . TokenAuthMethod ))
}
if raw , ok := settings [ SettingKeyOIDCConnectUsePKCE ]; ok {
result . OIDCConnectUsePKCE = raw == "true"
} else {
result . OIDCConnectUsePKCE = oidcUsePKCECompatibilityDefault ( oidcBase )
}
if raw , ok := settings [ SettingKeyOIDCConnectValidateIDToken ]; ok {
result . OIDCConnectValidateIDToken = raw == "true"
} else {
result . OIDCConnectValidateIDToken = oidcValidateIDTokenCompatibilityDefault ( oidcBase )
}
if v , ok := settings [ SettingKeyOIDCConnectAllowedSigningAlgs ]; ok && strings . TrimSpace ( v ) != "" {
result . OIDCConnectAllowedSigningAlgs = strings . TrimSpace ( v )
} else {
result . OIDCConnectAllowedSigningAlgs = strings . TrimSpace ( oidcBase . AllowedSigningAlgs )
}
clockSkewSet := false
if raw , ok := settings [ SettingKeyOIDCConnectClockSkewSeconds ]; ok && strings . TrimSpace ( raw ) != "" {
if parsed , err := strconv . Atoi ( strings . TrimSpace ( raw )); err == nil {
result . OIDCConnectClockSkewSeconds = parsed
clockSkewSet = true
}
}
if ! clockSkewSet {
result . OIDCConnectClockSkewSeconds = oidcBase . ClockSkewSeconds
}
if ! clockSkewSet && result . OIDCConnectClockSkewSeconds == 0 {
result . OIDCConnectClockSkewSeconds = 120
}
if raw , ok := settings [ SettingKeyOIDCConnectRequireEmailVerified ]; ok {
result . OIDCConnectRequireEmailVerified = raw == "true"
} else {
result . OIDCConnectRequireEmailVerified = oidcBase . RequireEmailVerified
}
if v , ok := settings [ SettingKeyOIDCConnectUserInfoEmailPath ]; ok {
result . OIDCConnectUserInfoEmailPath = strings . TrimSpace ( v )
} else {
result . OIDCConnectUserInfoEmailPath = strings . TrimSpace ( oidcBase . UserInfoEmailPath )
}
if v , ok := settings [ SettingKeyOIDCConnectUserInfoIDPath ]; ok {
result . OIDCConnectUserInfoIDPath = strings . TrimSpace ( v )
} else {
result . OIDCConnectUserInfoIDPath = strings . TrimSpace ( oidcBase . UserInfoIDPath )
}
if v , ok := settings [ SettingKeyOIDCConnectUserInfoUsernamePath ]; ok {
result . OIDCConnectUserInfoUsernamePath = strings . TrimSpace ( v )
} else {
result . OIDCConnectUserInfoUsernamePath = strings . TrimSpace ( oidcBase . UserInfoUsernamePath )
}
result . OIDCConnectClientSecret = strings . TrimSpace ( settings [ SettingKeyOIDCConnectClientSecret ])
if result . OIDCConnectClientSecret == "" {
result . OIDCConnectClientSecret = strings . TrimSpace ( oidcBase . ClientSecret )
}
result . OIDCConnectClientSecretConfigured = result . OIDCConnectClientSecret != ""
gitHubEffective := s . effectiveEmailOAuthConfig ( settings , "github" )
result . GitHubOAuthEnabled = gitHubEffective . Enabled
result . GitHubOAuthClientID = strings . TrimSpace ( gitHubEffective . ClientID )
result . GitHubOAuthClientSecret = strings . TrimSpace ( gitHubEffective . ClientSecret )
result . GitHubOAuthClientSecretConfigured = result . GitHubOAuthClientSecret != ""
result . GitHubOAuthRedirectURL = strings . TrimSpace ( gitHubEffective . RedirectURL )
result . GitHubOAuthFrontendRedirectURL = strings . TrimSpace ( gitHubEffective . FrontendRedirectURL )
googleEffective := s . effectiveEmailOAuthConfig ( settings , "google" )
result . GoogleOAuthEnabled = googleEffective . Enabled
result . GoogleOAuthClientID = strings . TrimSpace ( googleEffective . ClientID )
result . GoogleOAuthClientSecret = strings . TrimSpace ( googleEffective . ClientSecret )
result . GoogleOAuthClientSecretConfigured = result . GoogleOAuthClientSecret != ""
result . GoogleOAuthRedirectURL = strings . TrimSpace ( googleEffective . RedirectURL )
result . GoogleOAuthFrontendRedirectURL = strings . TrimSpace ( googleEffective . FrontendRedirectURL )
// WeChat Connect 设置:
// - 优先读取 DB 系统设置
// - 缺失时回退到 config/env,保持升级兼容
weChatEffective := s . effectiveWeChatConnectOAuthConfig ( settings )
result . WeChatConnectEnabled = weChatEffective . Enabled
result . WeChatConnectAppID = weChatEffective . LegacyAppID
result . WeChatConnectAppSecret = weChatEffective . LegacyAppSecret
result . WeChatConnectAppSecretConfigured = weChatEffective . LegacyAppSecret != ""
result . WeChatConnectOpenAppID = weChatEffective . OpenAppID
result . WeChatConnectOpenAppSecret = weChatEffective . OpenAppSecret
result . WeChatConnectOpenAppSecretConfigured = weChatEffective . OpenAppSecret != ""
result . WeChatConnectMPAppID = weChatEffective . MPAppID
result . WeChatConnectMPAppSecret = weChatEffective . MPAppSecret
result . WeChatConnectMPAppSecretConfigured = weChatEffective . MPAppSecret != ""
result . WeChatConnectMobileAppID = weChatEffective . MobileAppID
result . WeChatConnectMobileAppSecret = weChatEffective . MobileAppSecret
result . WeChatConnectMobileAppSecretConfigured = weChatEffective . MobileAppSecret != ""
result . WeChatConnectOpenEnabled = weChatEffective . OpenEnabled
result . WeChatConnectMPEnabled = weChatEffective . MPEnabled
result . WeChatConnectMobileEnabled = weChatEffective . MobileEnabled
result . WeChatConnectMode = weChatEffective . Mode
result . WeChatConnectScopes = weChatEffective . Scopes
result . WeChatConnectRedirectURL = weChatEffective . RedirectURL
result . WeChatConnectFrontendRedirectURL = weChatEffective . FrontendRedirectURL
// Model fallback settings
result . EnableModelFallback = settings [ SettingKeyEnableModelFallback ] == "true"
result . FallbackModelAnthropic = s . getStringOrDefault ( settings , SettingKeyFallbackModelAnthropic , "claude-3-5-sonnet-20241022" )
result . FallbackModelOpenAI = s . getStringOrDefault ( settings , SettingKeyFallbackModelOpenAI , "gpt-4o" )
result . FallbackModelGemini = s . getStringOrDefault ( settings , SettingKeyFallbackModelGemini , "gemini-2.5-pro" )
result . FallbackModelAntigravity = s . getStringOrDefault ( settings , SettingKeyFallbackModelAntigravity , "gemini-2.5-pro" )
// Identity patch settings (default: enabled, to preserve existing behavior)
if v , ok := settings [ SettingKeyEnableIdentityPatch ]; ok && v != "" {
result . EnableIdentityPatch = v == "true"
} else {
result . EnableIdentityPatch = true
}
result . IdentityPatchPrompt = settings [ SettingKeyIdentityPatchPrompt ]
// Ops monitoring settings (default: enabled, fail-open)
result . OpsMonitoringEnabled = ! isFalseSettingValue ( settings [ SettingKeyOpsMonitoringEnabled ])
result . OpsRealtimeMonitoringEnabled = ! isFalseSettingValue ( settings [ SettingKeyOpsRealtimeMonitoringEnabled ])
result . OpsQueryModeDefault = string ( ParseOpsQueryMode ( settings [ SettingKeyOpsQueryModeDefault ]))
result . OpsMetricsIntervalSeconds = 60
if raw := strings . TrimSpace ( settings [ SettingKeyOpsMetricsIntervalSeconds ]); raw != "" {
if v , err := strconv . Atoi ( raw ); err == nil {
if v < 60 {
v = 60
}
if v > 3600 {
v = 3600
}
result . OpsMetricsIntervalSeconds = v
}
}
// Channel monitor feature (default: enabled, 60s)
result . ChannelMonitorEnabled = ! isFalseSettingValue ( settings [ SettingKeyChannelMonitorEnabled ])
2026-08-07 11:01:22 +08:00
result . ChannelMonitorMode = normalizeChannelMonitorMode ( settings [ SettingKeyChannelMonitorMode ])
2026-07-07 23:35:47 +08:00
result . ChannelMonitorDefaultIntervalSeconds = parseChannelMonitorInterval (
settings [ SettingKeyChannelMonitorDefaultIntervalSeconds ],
)
2026-08-08 14:46:35 +08:00
// 默认隐藏吞吐(迁移 206 的隐私默认):未配置时必须与 setting_public.go 的
// 公开读取路径给出同一个值,否则管理端看到“未隐藏”而用户端实际已隐藏。
result . ChannelMonitorHideThroughput = ! isFalseSettingValue ( settings [ SettingKeyChannelMonitorHideThroughput ])
2026-08-18 04:06:18 +00:00
// 配额展示默认关闭且 fail-closed:仅字面 "true" 视为开启
// (与 setting_public.go 公开读取路径保持一致)。
result . ChannelMonitorShowQuota = settings [ SettingKeyChannelMonitorShowQuota ] == "true"
2026-09-07 18:31:03 +08:00
result . ChannelMonitorHideUserRanking = isTrueSettingValue ( settings [ SettingKeyChannelMonitorHideUserRanking ])
2026-07-07 23:35:47 +08:00
2026-08-07 12:31:14 +08:00
// Grok default mapping policy
result . GrokDefaultTextModel = strings . TrimSpace ( settings [ SettingKeyGrokDefaultTextModel ])
if result . GrokDefaultTextModel == "" {
2026-08-20 02:32:22 -07:00
result . GrokDefaultTextModel = "grok-4.6"
2026-08-07 12:31:14 +08:00
}
2026-08-08 14:39:22 +08:00
// Default true (missing/empty → enabled) so Claude/Codex→Grok mapping keeps working.
// Operators can set false to disable silent cross-client rewrite.
result . GrokCrossClientModelMapEnabled = ! isFalseSettingValue ( settings [ SettingKeyGrokCrossClientModelMapEnabled ])
2026-08-08 01:07:19 +08:00
result . GrokDefaultBaseURLMode = normalizeGrokDefaultBaseURLMode ( settings [ SettingKeyGrokDefaultBaseURLMode ])
2026-08-07 12:31:14 +08:00
2026-07-07 23:35:47 +08:00
// Available channels feature (default: disabled; strict true)
result . AvailableChannelsEnabled = settings [ SettingKeyAvailableChannelsEnabled ] == "true"
2026-09-10 23:27:28 +08:00
// Subscription feature (default: enabled; only an explicit false disables)
result . SubscriptionEnabled = ! isFalseSettingValue ( settings [ SettingKeySubscriptionEnabled ])
2026-07-28 15:55:56 +08:00
// Model plaza feature (default: disabled; strict true)
result . ModelPlazaEnabled = settings [ SettingKeyModelPlazaEnabled ] == "true"
result . ModelPlazaRequireAuth = settings [ SettingKeyModelPlazaRequireAuth ] == "true"
result . ModelPlazaDescription = settings [ SettingKeyModelPlazaDescription ]
2026-08-24 09:03:37 +08:00
result . PluginManagementEnabled = settings [ SettingKeyPluginManagementEnabled ] == "true"
2026-07-28 15:55:56 +08:00
2026-07-07 23:35:47 +08:00
// Affiliate (邀请返利) feature (default: disabled; strict true)
result . AffiliateEnabled = settings [ SettingKeyAffiliateEnabled ] == "true"
// 风控中心功能(默认关闭,严格 true 才启用)
result . RiskControlEnabled = settings [ SettingKeyRiskControlEnabled ] == "true"
// cyber 会话屏蔽(默认关闭,TTL 默认 3600s)
result . CyberSessionBlockEnabled = settings [ SettingKeyCyberSessionBlockEnabled ] == "true"
if v , err := strconv . Atoi ( strings . TrimSpace ( settings [ SettingKeyCyberSessionBlockTTLSeconds ])); err == nil && v > 0 {
result . CyberSessionBlockTTLSeconds = v
} else {
result . CyberSessionBlockTTLSeconds = 3600
}
// Claude Code version check
result . MinClaudeCodeVersion = settings [ SettingKeyMinClaudeCodeVersion ]
result . MaxClaudeCodeVersion = settings [ SettingKeyMaxClaudeCodeVersion ]
// 分组隔离
result . AllowUngroupedKeyScheduling = settings [ SettingKeyAllowUngroupedKeyScheduling ] == "true"
// Gateway forwarding behavior (defaults: fingerprint=true, metadata_passthrough=false,
// cch_signing=false, claude_oauth_system_prompt_injection=true)
2026-08-31 14:41:21 +08:00
result . OpenAITTFTMode = normalizeOpenAITTFTMode ( settings [ SettingKeyOpenAITTFTMode ])
2026-07-07 23:35:47 +08:00
if v , ok := settings [ SettingKeyEnableFingerprintUnification ]; ok && v != "" {
result . EnableFingerprintUnification = v == "true"
} else {
result . EnableFingerprintUnification = true // default: enabled (current behavior)
}
result . EnableMetadataPassthrough = settings [ SettingKeyEnableMetadataPassthrough ] == "true"
result . EnableCCHSigning = settings [ SettingKeyEnableCCHSigning ] == "true"
if v , ok := settings [ SettingKeyEnableClaudeOAuthSystemPromptInjection ]; ok && v != "" {
result . EnableClaudeOAuthSystemPromptInjection = v == "true"
} else {
result . EnableClaudeOAuthSystemPromptInjection = true
}
result . ClaudeOAuthSystemPrompt = settings [ SettingKeyClaudeOAuthSystemPrompt ]
result . ClaudeOAuthSystemPromptBlocks = settings [ SettingKeyClaudeOAuthSystemPromptBlocks ]
result . EnableAnthropicCacheTTL1hInjection = settings [ SettingKeyEnableAnthropicCacheTTL1hInjection ] == "true"
if v , ok := settings [ SettingKeyRewriteMessageCacheControl ]; ok && v != "" {
result . RewriteMessageCacheControl = v == "true"
} else {
result . RewriteMessageCacheControl = s . defaultRewriteMessageCacheControl ()
}
if v , ok := settings [ SettingKeyEnableClientDatelineNormalization ]; ok && v != "" {
result . EnableClientDatelineNormalization = v == "true"
} else {
result . EnableClientDatelineNormalization = true
}
result . AntigravityUserAgentVersion = antigravity . NormalizeUserAgentVersion ( settings [ SettingKeyAntigravityUserAgentVersion ])
result . OpenAICodexUserAgent = strings . TrimSpace ( settings [ SettingKeyOpenAICodexUserAgent ])
2026-08-03 20:14:58 +08:00
result . OpenAICodexClientVersion = NormalizeCodexClientVersion ( settings [ SettingKeyOpenAICodexClientVersion ])
result . OpenAICodexClientVersionSynced = NormalizeCodexClientVersion ( settings [ SettingKeyOpenAICodexClientVersionSynced ])
// 自动同步默认开启:缺失/空值一律视为开启,与 enable_client_dateline_normalization 同一惯例。
if v , ok := settings [ SettingKeyOpenAICodexVersionAutoSyncEnabled ]; ok && v != "" {
result . OpenAICodexVersionAutoSyncEnabled = v == "true"
} else {
result . OpenAICodexVersionAutoSyncEnabled = true
}
2026-07-07 23:35:47 +08:00
// codex_cli_only 加固
result . MinCodexVersion = settings [ SettingKeyMinCodexVersion ]
result . MaxCodexVersion = settings [ SettingKeyMaxCodexVersion ]
result . CodexCLIOnlyBlacklist = settings [ SettingKeyCodexCLIOnlyBlacklist ]
result . CodexCLIOnlyWhitelist = settings [ SettingKeyCodexCLIOnlyWhitelist ]
result . CodexCLIOnlyAllowAppServerClients = settings [ SettingKeyCodexCLIOnlyAllowAppServerClients ] == "true"
if raw := strings . TrimSpace ( settings [ SettingKeyCodexCLIOnlyEngineFingerprintSignals ]); raw != "" {
result . CodexCLIOnlyEngineFingerprintSignals = raw
} else {
result . CodexCLIOnlyEngineFingerprintSignals = openai . DefaultEngineFingerprintSignalsJSON () // 缺失/空 → 展示默认种子
}
// Web search emulation: quick enabled check from the JSON config
if raw := settings [ SettingKeyWebSearchEmulationConfig ]; raw != "" {
var wsCfg WebSearchEmulationConfig
if err := json . Unmarshal ([] byte ( raw ), & wsCfg ); err == nil {
result . WebSearchEmulationEnabled = wsCfg . Enabled && len ( wsCfg . Providers ) > 0
}
}
result . PaymentVisibleMethodAlipaySource = NormalizeVisibleMethodSource ( "alipay" , settings [ SettingPaymentVisibleMethodAlipaySource ])
result . PaymentVisibleMethodWxpaySource = NormalizeVisibleMethodSource ( "wxpay" , settings [ SettingPaymentVisibleMethodWxpaySource ])
result . PaymentVisibleMethodAlipayEnabled = settings [ SettingPaymentVisibleMethodAlipayEnabled ] == "true"
result . PaymentVisibleMethodWxpayEnabled = settings [ SettingPaymentVisibleMethodWxpayEnabled ] == "true"
2026-07-15 22:42:54 +08:00
result . OpenAILowUpstreamRatePriorityEnabled = settings [ SettingKeyOpenAILowUpstreamRatePriorityEnabled ] == "true"
result . OpenAIOAuthSchedulingRateMultiplier = parseOpenAIOAuthSchedulingRateMultiplier ( settings [ SettingKeyOpenAIOAuthSchedulingRateMultiplier ])
2026-07-07 23:35:47 +08:00
result . OpenAIAdvancedSchedulerEnabled = settings [ openAIAdvancedSchedulerSettingKey ] == "true"
result . OpenAIAdvancedSchedulerStickyWeightedEnabled = settings [ SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled ] == "true"
result . OpenAIAdvancedSchedulerSubscriptionPriorityEnabled = settings [ SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled ] == "true"
result . OpenAIAdvancedSchedulerLBTopK = strings . TrimSpace ( settings [ SettingKeyOpenAIAdvancedSchedulerLBTopK ])
result . OpenAIAdvancedSchedulerWeightPriority = strings . TrimSpace ( settings [ SettingKeyOpenAIAdvancedSchedulerWeightPriority ])
result . OpenAIAdvancedSchedulerWeightLoad = strings . TrimSpace ( settings [ SettingKeyOpenAIAdvancedSchedulerWeightLoad ])
result . OpenAIAdvancedSchedulerWeightQueue = strings . TrimSpace ( settings [ SettingKeyOpenAIAdvancedSchedulerWeightQueue ])
result . OpenAIAdvancedSchedulerWeightErrorRate = strings . TrimSpace ( settings [ SettingKeyOpenAIAdvancedSchedulerWeightErrorRate ])
result . OpenAIAdvancedSchedulerWeightTTFT = strings . TrimSpace ( settings [ SettingKeyOpenAIAdvancedSchedulerWeightTTFT ])
result . OpenAIAdvancedSchedulerWeightReset = strings . TrimSpace ( settings [ SettingKeyOpenAIAdvancedSchedulerWeightReset ])
result . OpenAIAdvancedSchedulerWeightQuotaHeadroom = strings . TrimSpace ( settings [ SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom ])
2026-07-15 22:42:54 +08:00
result . OpenAIAdvancedSchedulerWeightUpstreamCost = strings . TrimSpace ( settings [ SettingKeyOpenAIAdvancedSchedulerWeightUpstreamCost ])
2026-07-07 23:35:47 +08:00
result . OpenAIAdvancedSchedulerWeightPreviousResponse = strings . TrimSpace ( settings [ SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse ])
result . OpenAIAdvancedSchedulerWeightSessionSticky = strings . TrimSpace ( settings [ SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky ])
result . OpenAIAdvancedSchedulerEffectiveLBTopK = s . openAIAdvancedSchedulerEffectiveLBTopK ()
effectiveWeights := s . openAIAdvancedSchedulerEffectiveWeights ()
result . OpenAIAdvancedSchedulerEffectiveWeightPriority = formatOpenAIAdvancedSchedulerFloat ( effectiveWeights . Priority )
result . OpenAIAdvancedSchedulerEffectiveWeightLoad = formatOpenAIAdvancedSchedulerFloat ( effectiveWeights . Load )
result . OpenAIAdvancedSchedulerEffectiveWeightQueue = formatOpenAIAdvancedSchedulerFloat ( effectiveWeights . Queue )
result . OpenAIAdvancedSchedulerEffectiveWeightErrorRate = formatOpenAIAdvancedSchedulerFloat ( effectiveWeights . ErrorRate )
result . OpenAIAdvancedSchedulerEffectiveWeightTTFT = formatOpenAIAdvancedSchedulerFloat ( effectiveWeights . TTFT )
result . OpenAIAdvancedSchedulerEffectiveWeightReset = formatOpenAIAdvancedSchedulerFloat ( effectiveWeights . Reset )
result . OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom = formatOpenAIAdvancedSchedulerFloat ( effectiveWeights . QuotaHeadroom )
2026-07-15 22:42:54 +08:00
result . OpenAIAdvancedSchedulerEffectiveWeightUpstreamCost = formatOpenAIAdvancedSchedulerFloat ( effectiveWeights . UpstreamCost )
2026-07-07 23:35:47 +08:00
result . OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse = formatOpenAIAdvancedSchedulerFloat ( effectiveWeights . PreviousResponse )
result . OpenAIAdvancedSchedulerEffectiveWeightSessionSticky = formatOpenAIAdvancedSchedulerFloat ( effectiveWeights . SessionSticky )
// 余额、订阅到期与账号限额通知
result . BalanceLowNotifyEnabled = settings [ SettingKeyBalanceLowNotifyEnabled ] == "true"
if v , err := strconv . ParseFloat ( settings [ SettingKeyBalanceLowNotifyThreshold ], 64 ); err == nil && v >= 0 {
result . BalanceLowNotifyThreshold = v
}
result . BalanceLowNotifyRechargeURL = settings [ SettingKeyBalanceLowNotifyRechargeURL ]
result . SubscriptionExpiryNotifyEnabled = ! isFalseSettingValue ( settings [ SettingKeySubscriptionExpiryNotifyEnabled ])
// 账号限额通知
result . AccountQuotaNotifyEnabled = settings [ SettingKeyAccountQuotaNotifyEnabled ] == "true"
if raw := strings . TrimSpace ( settings [ SettingKeyAccountQuotaNotifyEmails ]); raw != "" {
result . AccountQuotaNotifyEmails = ParseNotifyEmails ( raw )
}
if result . AccountQuotaNotifyEmails == nil {
result . AccountQuotaNotifyEmails = [] NotifyEmailEntry {}
}
// 系统层默认 platform quota(修复 Bug B: parseSettings 不填充导致回显恒为 nil)
if raw := settings [ SettingKeyDefaultPlatformQuotas ]; raw != "" {
parsed := map [ string ] * DefaultPlatformQuotaSetting {}
if err := json . Unmarshal ([] byte ( raw ), & parsed ); err != nil {
slog . Warn ( "[Setting] parseSettings: unmarshal default_platform_quotas failed" , "error" , err )
} else {
result . DefaultPlatformQuotas = parsed
}
}
2026-08-07 14:53:18 +08:00
result . AccountSchedulingThresholds = defaultAccountSchedulingThresholds ()
if raw := strings . TrimSpace ( settings [ SettingKeyAccountSchedulingThresholds ]); raw != "" {
if thresholds , err := parseAccountSchedulingThresholdsSetting ( raw ); err != nil {
slog . Warn ( "[Setting] parseSettings: unmarshal account_scheduling_thresholds failed" , "error" , err )
} else {
result . AccountSchedulingThresholds = thresholds
}
}
2026-07-07 23:35:47 +08:00
result . AllowUserViewErrorRequests = settings [ SettingKeyAllowUserViewErrorRequests ] == "true" // default false
2026-08-07 12:31:14 +08:00
// Publish Grok default model_mapping options for accounts with empty mapping.
xai . SetRuntimeModelMappingOptions ( xai . ModelMappingOptions {
DefaultText : result . GrokDefaultTextModel ,
EnableCrossClientMap : result . GrokCrossClientModelMapEnabled ,
})
2026-07-07 23:35:47 +08:00
return result
}
2026-08-31 14:41:21 +08:00
func normalizeOpenAITTFTMode ( mode string ) string {
if strings . EqualFold ( strings . TrimSpace ( mode ), OpenAITTFTModeVisible ) {
return OpenAITTFTModeVisible
}
return OpenAITTFTModeSemantic
}
2026-07-07 23:35:47 +08:00
func clampAffiliateRebateRate ( value float64 ) float64 {
if math . IsNaN ( value ) || math . IsInf ( value , 0 ) {
return AffiliateRebateRateDefault
}
if value < AffiliateRebateRateMin {
return AffiliateRebateRateMin
}
if value > AffiliateRebateRateMax {
return AffiliateRebateRateMax
}
return value
}
func isFalseSettingValue ( value string ) bool {
switch strings . ToLower ( strings . TrimSpace ( value )) {
case "false" , "0" , "off" , "disabled" :
return true
default :
return false
}
}
2026-09-07 18:31:03 +08:00
func isTrueSettingValue ( value string ) bool {
switch strings . ToLower ( strings . TrimSpace ( value )) {
case "true" , "1" , "on" , "enabled" :
return true
default :
return false
}
}
2026-07-07 23:35:47 +08:00
func normalizeVisibleMethodSettingSource ( method , source string , enabled bool ) ( string , error ) {
_ = enabled
source = strings . TrimSpace ( source )
if source == "" {
return "" , nil
}
normalized := NormalizeVisibleMethodSource ( method , source )
if normalized == "" {
return "" , infraerrors . BadRequest (
"INVALID_PAYMENT_VISIBLE_METHOD_SOURCE" ,
fmt . Sprintf ( "%s source must be one of the supported payment providers" , method ),
)
}
return normalized , nil
}
func ( s * SettingService ) openAIAdvancedSchedulerEffectiveLBTopK () string {
if s != nil && s . cfg != nil && s . cfg . Gateway . OpenAIWS . LBTopK > 0 {
return strconv . Itoa ( s . cfg . Gateway . OpenAIWS . LBTopK )
}
return "7"
}
func ( s * SettingService ) openAIAdvancedSchedulerEffectiveWeights () config . GatewayOpenAIWSSchedulerScoreWeights {
defaults := config . GatewayOpenAIWSSchedulerScoreWeights {
Priority : 1.0 ,
Load : 1.0 ,
Queue : 0.7 ,
ErrorRate : 0.8 ,
TTFT : 0.5 ,
Reset : 0.0 ,
QuotaHeadroom : 0.0 ,
2026-07-15 22:42:54 +08:00
UpstreamCost : 0.0 ,
2026-07-07 23:35:47 +08:00
PreviousResponse : 5.0 ,
SessionSticky : 3.0 ,
}
if s == nil || s . cfg == nil {
return defaults
}
weights := s . cfg . Gateway . OpenAIWS . SchedulerScoreWeights
2026-07-15 22:42:54 +08:00
if ! weights . IsValid () {
2026-07-07 23:35:47 +08:00
return defaults
}
return weights
}
func formatOpenAIAdvancedSchedulerFloat ( value float64 ) string {
return strconv . FormatFloat ( value , 'f' , - 1 , 64 )
}
func ( s * SettingService ) normalizeOpenAIAdvancedSchedulerOverrides ( settings * SystemSettings ) error {
2026-07-15 22:42:54 +08:00
if rate := settings . OpenAIOAuthSchedulingRateMultiplier ; rate < 0 || math . IsNaN ( rate ) || math . IsInf ( rate , 0 ) {
return infraerrors . BadRequest ( "INVALID_OPENAI_OAUTH_SCHEDULING_RATE_MULTIPLIER" , "OpenAI OAuth scheduling rate multiplier must be a finite non-negative number" )
}
2026-07-07 23:35:47 +08:00
lbTopK , err := normalizeOptionalPositiveIntString ( settings . OpenAIAdvancedSchedulerLBTopK )
if err != nil {
return infraerrors . BadRequest ( "INVALID_OPENAI_ADVANCED_SCHEDULER_LB_TOP_K" , "openai advanced scheduler TopK must be a positive integer or empty" )
}
settings . OpenAIAdvancedSchedulerLBTopK = lbTopK
weights := [] * string {
& settings . OpenAIAdvancedSchedulerWeightPriority ,
& settings . OpenAIAdvancedSchedulerWeightLoad ,
& settings . OpenAIAdvancedSchedulerWeightQueue ,
& settings . OpenAIAdvancedSchedulerWeightErrorRate ,
& settings . OpenAIAdvancedSchedulerWeightTTFT ,
& settings . OpenAIAdvancedSchedulerWeightReset ,
& settings . OpenAIAdvancedSchedulerWeightQuotaHeadroom ,
2026-07-15 22:42:54 +08:00
& settings . OpenAIAdvancedSchedulerWeightUpstreamCost ,
2026-07-07 23:35:47 +08:00
& settings . OpenAIAdvancedSchedulerWeightPreviousResponse ,
& settings . OpenAIAdvancedSchedulerWeightSessionSticky ,
}
for _ , target := range weights {
normalized , err := normalizeOptionalNonNegativeFloatString ( * target )
if err != nil {
return infraerrors . BadRequest ( "INVALID_OPENAI_ADVANCED_SCHEDULER_WEIGHT" , "openai advanced scheduler weights must be non-negative numbers or empty" )
}
* target = normalized
}
// 与 config.Validate 的 "scheduler_score_weights must not all be zero" 保持一致:
// 覆盖值(空则回退到生效的配置值)叠加后的基础权重和不允许为 0,
// 否则调度会静默退化为 TopK 内均匀随机。
effective := s . openAIAdvancedSchedulerEffectiveWeights ()
2026-07-15 22:42:54 +08:00
resolved := config . GatewayOpenAIWSSchedulerScoreWeights {
Priority : resolveOpenAIAdvancedSchedulerWeight ( settings . OpenAIAdvancedSchedulerWeightPriority , effective . Priority ),
Load : resolveOpenAIAdvancedSchedulerWeight ( settings . OpenAIAdvancedSchedulerWeightLoad , effective . Load ),
Queue : resolveOpenAIAdvancedSchedulerWeight ( settings . OpenAIAdvancedSchedulerWeightQueue , effective . Queue ),
ErrorRate : resolveOpenAIAdvancedSchedulerWeight ( settings . OpenAIAdvancedSchedulerWeightErrorRate , effective . ErrorRate ),
TTFT : resolveOpenAIAdvancedSchedulerWeight ( settings . OpenAIAdvancedSchedulerWeightTTFT , effective . TTFT ),
Reset : resolveOpenAIAdvancedSchedulerWeight ( settings . OpenAIAdvancedSchedulerWeightReset , effective . Reset ),
QuotaHeadroom : resolveOpenAIAdvancedSchedulerWeight ( settings . OpenAIAdvancedSchedulerWeightQuotaHeadroom , effective . QuotaHeadroom ),
UpstreamCost : resolveOpenAIAdvancedSchedulerWeight ( settings . OpenAIAdvancedSchedulerWeightUpstreamCost , effective . UpstreamCost ),
PreviousResponse : resolveOpenAIAdvancedSchedulerWeight ( settings . OpenAIAdvancedSchedulerWeightPreviousResponse , effective . PreviousResponse ),
SessionSticky : resolveOpenAIAdvancedSchedulerWeight ( settings . OpenAIAdvancedSchedulerWeightSessionSticky , effective . SessionSticky ),
}
if ! resolved . IsValid () {
return infraerrors . BadRequest ( "INVALID_OPENAI_ADVANCED_SCHEDULER_WEIGHT" , "openai advanced scheduler weights must have finite non-zero base and total sums" )
2026-07-07 23:35:47 +08:00
}
return nil
}
2026-07-15 22:42:54 +08:00
func parseOpenAIOAuthSchedulingRateMultiplier ( raw string ) float64 {
value , err := strconv . ParseFloat ( strings . TrimSpace ( raw ), 64 )
if err != nil || value < 0 || math . IsNaN ( value ) || math . IsInf ( value , 0 ) {
return defaultOpenAIOAuthSchedulingRateMultiplier
}
return value
}
2026-07-07 23:35:47 +08:00
// resolveOpenAIAdvancedSchedulerWeight 返回覆盖值(已归一化的非空字符串),空则回退默认值。
func resolveOpenAIAdvancedSchedulerWeight ( normalized string , fallback float64 ) float64 {
if normalized == "" {
return fallback
}
value , err := strconv . ParseFloat ( normalized , 64 )
if err != nil {
return fallback
}
return value
}
func normalizeOptionalPositiveIntString ( raw string ) ( string , error ) {
raw = strings . TrimSpace ( raw )
if raw == "" {
return "" , nil
}
value , err := strconv . Atoi ( raw )
if err != nil || value <= 0 {
return "" , fmt . Errorf ( "invalid positive integer" )
}
return strconv . Itoa ( value ), nil
}
func normalizeOptionalNonNegativeFloatString ( raw string ) ( string , error ) {
raw = strings . TrimSpace ( raw )
if raw == "" {
return "" , nil
}
value , err := strconv . ParseFloat ( raw , 64 )
if err != nil || value < 0 || math . IsNaN ( value ) || math . IsInf ( value , 0 ) {
return "" , fmt . Errorf ( "invalid non-negative float" )
}
return strconv . FormatFloat ( value , 'f' , - 1 , 64 ), nil
}
func parseDefaultSubscriptions ( raw string ) [] DefaultSubscriptionSetting {
raw = strings . TrimSpace ( raw )
if raw == "" {
return nil
}
var items [] DefaultSubscriptionSetting
if err := json . Unmarshal ([] byte ( raw ), & items ); err != nil {
return nil
}
normalized := make ([] DefaultSubscriptionSetting , 0 , len ( items ))
for _ , item := range items {
if item . GroupID <= 0 || item . ValidityDays <= 0 {
continue
}
if item . ValidityDays > MaxValidityDays {
item . ValidityDays = MaxValidityDays
}
normalized = append ( normalized , item )
}
return normalized
}
func parseProviderDefaultGrantSettings ( settings map [ string ] string , keys authSourceDefaultKeySet ) ProviderDefaultGrantSettings {
result := ProviderDefaultGrantSettings {
Balance : defaultAuthSourceBalance ,
Concurrency : defaultAuthSourceConcurrency ,
Subscriptions : [] DefaultSubscriptionSetting {},
GrantOnSignup : false ,
GrantOnFirstBind : false ,
}
if v , err := strconv . ParseFloat ( strings . TrimSpace ( settings [ keys . balance ]), 64 ); err == nil {
result . Balance = v
}
if v , err := strconv . Atoi ( strings . TrimSpace ( settings [ keys . concurrency ])); err == nil {
result . Concurrency = v
}
if items := parseDefaultSubscriptions ( settings [ keys . subscriptions ]); items != nil {
result . Subscriptions = items
}
if raw , ok := settings [ keys . grantOnSignup ]; ok {
result . GrantOnSignup = raw == "true"
}
if raw , ok := settings [ keys . grantOnFirstBind ]; ok {
result . GrantOnFirstBind = raw == "true"
}
if raw := settings [ keys . platformQuotas ]; raw != "" {
parsed := map [ string ] * DefaultPlatformQuotaSetting {}
if err := json . Unmarshal ([] byte ( raw ), & parsed ); err != nil {
slog . Warn ( "[Setting] parseProviderDefaultGrantSettings: unmarshal auth source platform quotas failed" , "source" , keys . source , "error" , err )
} else {
result . PlatformQuotas = parsed
}
}
return result
}
func writeProviderDefaultGrantUpdates ( updates map [ string ] string , keys authSourceDefaultKeySet , settings ProviderDefaultGrantSettings ) {
updates [ keys . balance ] = strconv . FormatFloat ( settings . Balance , 'f' , 8 , 64 )
updates [ keys . concurrency ] = strconv . Itoa ( settings . Concurrency )
subscriptions := settings . Subscriptions
if subscriptions == nil {
subscriptions = [] DefaultSubscriptionSetting {}
}
raw , err := json . Marshal ( subscriptions )
if err != nil {
raw = [] byte ( "[]" )
}
updates [ keys . subscriptions ] = string ( raw )
updates [ keys . grantOnSignup ] = strconv . FormatBool ( settings . GrantOnSignup )
updates [ keys . grantOnFirstBind ] = strconv . FormatBool ( settings . GrantOnFirstBind )
// auth source platform quota:整体替换语义。
// nil = 请求未携带该字段,跳过写入以保留既有配置(与系统层 buildSystemSettingsUpdates 的
// DefaultPlatformQuotas nil 守卫一致);非 nil(含空 map)才整体替换。二者语义不可混同。
if keys . platformQuotas != "" && settings . PlatformQuotas != nil {
blob , err := json . Marshal ( settings . PlatformQuotas )
if err != nil {
blob = [] byte ( "{}" )
}
updates [ keys . platformQuotas ] = string ( blob )
}
}
func mergeProviderDefaultGrantSettings ( globalDefaults ProviderDefaultGrantSettings , providerDefaults ProviderDefaultGrantSettings ) ProviderDefaultGrantSettings {
result := ProviderDefaultGrantSettings {
Balance : globalDefaults . Balance ,
Concurrency : globalDefaults . Concurrency ,
Subscriptions : append ([] DefaultSubscriptionSetting ( nil ), globalDefaults . Subscriptions ... ),
GrantOnSignup : providerDefaults . GrantOnSignup ,
GrantOnFirstBind : providerDefaults . GrantOnFirstBind ,
}
// 注意:不能把 parse 默认值 (defaultAuthSourceBalance / defaultAuthSourceConcurrency)
// 当作"未配置"哨兵——admin 完全有权显式设成相同的值,那时仍应覆盖 globalDefaults。
// 旧实现的 `!= defaultAuthSourceConcurrency` 会把 admin 设的 5 与 fallback 5 混淆,
// 导致渠道发放退回到全局默认(如 1),表现为"管理员设 5、新用户实际拿 1"。
if providerDefaults . Balance >= 0 {
result . Balance = providerDefaults . Balance
}
if providerDefaults . Concurrency > 0 {
result . Concurrency = providerDefaults . Concurrency
}
if len ( providerDefaults . Subscriptions ) > 0 {
result . Subscriptions = append ([] DefaultSubscriptionSetting ( nil ), providerDefaults . Subscriptions ... )
}
return result
}
func parseTablePreferences ( defaultPageSizeRaw , optionsRaw string ) ( int , [] int ) {
defaultPageSize := 20
if v , err := strconv . Atoi ( strings . TrimSpace ( defaultPageSizeRaw )); err == nil {
defaultPageSize = v
}
var options [] int
if strings . TrimSpace ( optionsRaw ) != "" {
_ = json . Unmarshal ([] byte ( optionsRaw ), & options )
}
return normalizeTablePreferences ( defaultPageSize , options )
}
func normalizeTablePreferences ( defaultPageSize int , options [] int ) ( int , [] int ) {
const minPageSize = 5
const maxPageSize = 1000
const fallbackPageSize = 20
seen := make ( map [ int ] struct {}, len ( options ))
normalizedOptions := make ([] int , 0 , len ( options ))
for _ , option := range options {
if option < minPageSize || option > maxPageSize {
continue
}
if _ , ok := seen [ option ]; ok {
continue
}
seen [ option ] = struct {}{}
normalizedOptions = append ( normalizedOptions , option )
}
sort . Ints ( normalizedOptions )
if defaultPageSize < minPageSize || defaultPageSize > maxPageSize {
defaultPageSize = fallbackPageSize
}
if len ( normalizedOptions ) == 0 {
normalizedOptions = [] int { 10 , 20 , 50 }
}
return defaultPageSize , normalizedOptions
}