Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
1310 lines
63 KiB
Go
1310 lines
63 KiB
Go
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",
|
||
|
||
// Grok: safe defaults — no cross-vendor model rewrite unless operators enable it.
|
||
SettingKeyGrokDefaultTextModel: "grok-4.5",
|
||
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: "",
|
||
|
||
// 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"
|
||
|
||
// Grok default mapping policy
|
||
result.GrokDefaultTextModel = strings.TrimSpace(settings[SettingKeyGrokDefaultTextModel])
|
||
if result.GrokDefaultTextModel == "" {
|
||
result.GrokDefaultTextModel = "grok-4.5"
|
||
}
|
||
// 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]
|
||
|
||
// 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)
|
||
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 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 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
|
||
}
|