| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331 |
- 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"
- "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
- )
- // 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
- }
- 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)
- }
- // 初始化默认设置
- defaults := map[string]string{
- SettingKeyRegistrationEnabled: "true",
- SettingKeyEmailVerifyEnabled: "false",
- SettingKeyRegistrationEmailSuffixWhitelist: "[]",
- SettingKeyRegistrationEmailDomainQuotaEnabled: "false",
- SettingKeyPromoCodeEnabled: "true", // 默认启用优惠码功能
- SettingKeyLoginAgreementEnabled: "false",
- SettingKeyLoginAgreementMode: defaultLoginAgreementMode,
- SettingKeyLoginAgreementUpdatedAt: defaultLoginAgreementDate,
- SettingKeyLoginAgreementDocuments: loginAgreementDocumentsJSON,
- SettingKeyAPIKeyACLTrustForwardedIP: "true",
- SettingKeyForwardedClientIPHeaders: string(forwardedClientIPHeadersJSON),
- settingKeyForwardedClientIPModeV2: "true",
- 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",
- SettingKeyChannelMonitorMode: ChannelMonitorModeV1,
- SettingKeyChannelMonitorDefaultIntervalSeconds: "60",
- SettingKeyChannelMonitorHideThroughput: "true",
- SettingKeyChannelMonitorShowQuota: "false",
- SettingKeyChannelMonitorHideUserRanking: "false",
- // Grok compatibility defaults: cross-client mapping stays enabled unless
- // operators explicitly disable it.
- SettingKeyGrokDefaultTextModel: "grok-4.6",
- SettingKeyGrokCrossClientModelMapEnabled: "true",
- SettingKeyGrokDefaultBaseURLMode: GrokDefaultBaseURLModeCLI,
- // Available channels feature (default disabled; opt-in)
- SettingKeyAvailableChannelsEnabled: "false",
- // Model plaza feature (default disabled; opt-in, public unless require_auth)
- SettingKeyModelPlazaEnabled: "false",
- SettingKeyModelPlazaRequireAuth: "false",
- SettingKeyModelPlazaDescription: "",
- SettingKeyPluginManagementEnabled: "false",
- // Affiliate (邀请返利) feature (default disabled; opt-in)
- SettingKeyAffiliateEnabled: "false",
- SettingKeyAffiliateAdminRechargeEnabled: strconv.FormatBool(AdminRechargeRebateEnabledDefault),
- // 风控中心功能(默认关闭,显式启用)
- 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",
- SettingKeyOpenAILowUpstreamRatePriorityEnabled: "false",
- SettingKeyOpenAIOAuthSchedulingRateMultiplier: "1",
- SettingKeyEnableAnthropicCacheTTL1hInjection: "false",
- SettingKeyRewriteMessageCacheControl: strconv.FormatBool(s.defaultRewriteMessageCacheControl()),
- SettingKeyEnableClientDatelineNormalization: "true",
- SettingKeyAntigravityUserAgentVersion: "",
- SettingKeyOpenAICodexUserAgent: "",
- SettingKeyOpenAICodexClientVersion: "",
- SettingKeyOpenAICodexClientVersionSynced: "",
- SettingKeyOpenAICodexVersionAutoSyncEnabled: "true",
- SettingPaymentVisibleMethodAlipaySource: "",
- SettingPaymentVisibleMethodWxpaySource: "",
- SettingPaymentVisibleMethodAlipayEnabled: "false",
- SettingPaymentVisibleMethodWxpayEnabled: "false",
- openAIAdvancedSchedulerSettingKey: "false",
- SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled: "false",
- SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled: "false",
- SettingKeyOpenAIAdvancedSchedulerLBTopK: "",
- SettingKeyOpenAIAdvancedSchedulerWeightPriority: "",
- SettingKeyOpenAIAdvancedSchedulerWeightLoad: "",
- SettingKeyOpenAIAdvancedSchedulerWeightQueue: "",
- SettingKeyOpenAIAdvancedSchedulerWeightErrorRate: "",
- SettingKeyOpenAIAdvancedSchedulerWeightTTFT: "",
- SettingKeyOpenAIAdvancedSchedulerWeightReset: "",
- SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom: "",
- SettingKeyOpenAIAdvancedSchedulerWeightUpstreamCost: "",
- SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse: "",
- SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky: "",
- SettingKeyAllowUserViewErrorRequests: "false",
- }
- return s.settingRepo.SetMultiple(ctx, defaults)
- }
- 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
- }
- // 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
- forwardedClientIPHeaders := []string{}
- if s != nil && s.cfg != nil {
- runtimeSettings := s.cfg.ForwardedClientIPSettings()
- apiKeyACLTrustForwardedIP = runtimeSettings.TrustForwardedIP
- forwardedClientIPHeaders = runtimeSettings.Headers
- }
- if value, ok := settings[SettingKeyAPIKeyACLTrustForwardedIP]; ok {
- apiKeyACLTrustForwardedIP = value == "true"
- }
- 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
- }
- }
- result := &SystemSettings{
- RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
- EmailVerifyEnabled: emailVerifyEnabled,
- RegistrationEmailSuffixWhitelist: ParseRegistrationEmailSuffixWhitelist(settings[SettingKeyRegistrationEmailSuffixWhitelist]),
- RegistrationEmailDomainQuotaEnabled: settings[SettingKeyRegistrationEmailDomainQuotaEnabled] == "true",
- 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] != "",
- TencentCaptchaRegion: normalizeTencentCaptchaRegion(settings[SettingKeyTencentCaptchaRegion]),
- AliyunCaptchaEnabled: settings[SettingKeyAliyunCaptchaEnabled] == "true",
- AliyunCaptchaAccessKeyID: settings[SettingKeyAliyunCaptchaAccessKeyID],
- AliyunCaptchaAccessKeySecretConfigured: settings[SettingKeyAliyunCaptchaAccessKeySecret] != "",
- AliyunCaptchaSceneID: settings[SettingKeyAliyunCaptchaSceneID],
- AliyunCaptchaPrefix: settings[SettingKeyAliyunCaptchaPrefix],
- AliyunCaptchaRegion: normalizeAliyunCaptchaRegion(settings[SettingKeyAliyunCaptchaRegion]),
- 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",
- }
- 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
- }
- result.AdminRechargeRebateEnabled = settings[SettingKeyAffiliateAdminRechargeEnabled] == "true"
- result.DefaultSubscriptions = parseDefaultSubscriptions(settings[SettingKeyDefaultSubscriptions])
- // 敏感信息直接返回,方便测试连接时使用
- result.SMTPPassword = settings[SettingKeySMTPPassword]
- result.TurnstileSecretKey = settings[SettingKeyTurnstileSecretKey]
- result.TencentCaptchaAppSecretKey = settings[SettingKeyTencentCaptchaAppSecretKey]
- result.TencentCaptchaCloudSecretID = settings[SettingKeyTencentCaptchaCloudSecretID]
- result.TencentCaptchaCloudSecretKey = settings[SettingKeyTencentCaptchaCloudSecretKey]
- result.AliyunCaptchaAccessKeySecret = settings[SettingKeyAliyunCaptchaAccessKeySecret]
- // 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])
- result.ChannelMonitorMode = normalizeChannelMonitorMode(settings[SettingKeyChannelMonitorMode])
- result.ChannelMonitorDefaultIntervalSeconds = parseChannelMonitorInterval(
- settings[SettingKeyChannelMonitorDefaultIntervalSeconds],
- )
- // 默认隐藏吞吐(迁移 206 的隐私默认):未配置时必须与 setting_public.go 的
- // 公开读取路径给出同一个值,否则管理端看到“未隐藏”而用户端实际已隐藏。
- result.ChannelMonitorHideThroughput = !isFalseSettingValue(settings[SettingKeyChannelMonitorHideThroughput])
- // 配额展示默认关闭且 fail-closed:仅字面 "true" 视为开启
- // (与 setting_public.go 公开读取路径保持一致)。
- result.ChannelMonitorShowQuota = settings[SettingKeyChannelMonitorShowQuota] == "true"
- result.ChannelMonitorHideUserRanking = isTrueSettingValue(settings[SettingKeyChannelMonitorHideUserRanking])
- // Grok default mapping policy
- result.GrokDefaultTextModel = strings.TrimSpace(settings[SettingKeyGrokDefaultTextModel])
- if result.GrokDefaultTextModel == "" {
- result.GrokDefaultTextModel = "grok-4.6"
- }
- // 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])
- result.GrokDefaultBaseURLMode = normalizeGrokDefaultBaseURLMode(settings[SettingKeyGrokDefaultBaseURLMode])
- // Available channels feature (default: disabled; strict true)
- result.AvailableChannelsEnabled = settings[SettingKeyAvailableChannelsEnabled] == "true"
- // Model plaza feature (default: disabled; strict true)
- result.ModelPlazaEnabled = settings[SettingKeyModelPlazaEnabled] == "true"
- result.ModelPlazaRequireAuth = settings[SettingKeyModelPlazaRequireAuth] == "true"
- result.ModelPlazaDescription = settings[SettingKeyModelPlazaDescription]
- result.PluginManagementEnabled = settings[SettingKeyPluginManagementEnabled] == "true"
- // 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)
- result.OpenAITTFTMode = normalizeOpenAITTFTMode(settings[SettingKeyOpenAITTFTMode])
- 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])
- 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
- }
- // 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"
- result.OpenAILowUpstreamRatePriorityEnabled = settings[SettingKeyOpenAILowUpstreamRatePriorityEnabled] == "true"
- result.OpenAIOAuthSchedulingRateMultiplier = parseOpenAIOAuthSchedulingRateMultiplier(settings[SettingKeyOpenAIOAuthSchedulingRateMultiplier])
- 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])
- result.OpenAIAdvancedSchedulerWeightUpstreamCost = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightUpstreamCost])
- 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)
- result.OpenAIAdvancedSchedulerEffectiveWeightUpstreamCost = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.UpstreamCost)
- 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
- }
- }
- 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
- }
- }
- result.AllowUserViewErrorRequests = settings[SettingKeyAllowUserViewErrorRequests] == "true" // default false
- // Publish Grok default model_mapping options for accounts with empty mapping.
- xai.SetRuntimeModelMappingOptions(xai.ModelMappingOptions{
- DefaultText: result.GrokDefaultTextModel,
- EnableCrossClientMap: result.GrokCrossClientModelMapEnabled,
- })
- return result
- }
- func normalizeOpenAITTFTMode(mode string) string {
- if strings.EqualFold(strings.TrimSpace(mode), OpenAITTFTModeVisible) {
- return OpenAITTFTModeVisible
- }
- return OpenAITTFTModeSemantic
- }
- 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
- }
- }
- func isTrueSettingValue(value string) bool {
- switch strings.ToLower(strings.TrimSpace(value)) {
- case "true", "1", "on", "enabled":
- return true
- default:
- return false
- }
- }
- 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,
- UpstreamCost: 0.0,
- PreviousResponse: 5.0,
- SessionSticky: 3.0,
- }
- if s == nil || s.cfg == nil {
- return defaults
- }
- weights := s.cfg.Gateway.OpenAIWS.SchedulerScoreWeights
- if !weights.IsValid() {
- return defaults
- }
- return weights
- }
- func formatOpenAIAdvancedSchedulerFloat(value float64) string {
- return strconv.FormatFloat(value, 'f', -1, 64)
- }
- func (s *SettingService) normalizeOpenAIAdvancedSchedulerOverrides(settings *SystemSettings) error {
- 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")
- }
- 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,
- &settings.OpenAIAdvancedSchedulerWeightUpstreamCost,
- &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()
- 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")
- }
- return nil
- }
- 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
- }
- // 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
- }
|