setting_parse.go 64 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "log/slog"
  8. "math"
  9. "sort"
  10. "strconv"
  11. "strings"
  12. "github.com/Wei-Shaw/sub2api/internal/config"
  13. "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
  14. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  15. "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
  16. "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
  17. )
  18. // InitializeDefaultSettings 初始化默认设置
  19. func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
  20. // 检查是否已有设置
  21. _, err := s.settingRepo.GetValue(ctx, SettingKeyRegistrationEnabled)
  22. if err == nil {
  23. // 已有设置,不需要初始化
  24. return nil
  25. }
  26. if !errors.Is(err, ErrSettingNotFound) {
  27. return fmt.Errorf("check existing settings: %w", err)
  28. }
  29. oidcUsePKCEDefault := true
  30. oidcValidateIDTokenDefault := true
  31. if s != nil && s.cfg != nil {
  32. if s.cfg.OIDC.UsePKCEExplicit {
  33. oidcUsePKCEDefault = s.cfg.OIDC.UsePKCE
  34. }
  35. if s.cfg.OIDC.ValidateIDTokenExplicit {
  36. oidcValidateIDTokenDefault = s.cfg.OIDC.ValidateIDToken
  37. }
  38. }
  39. loginAgreementDocumentsJSON, err := marshalLoginAgreementDocuments(defaultLoginAgreementDocuments())
  40. if err != nil {
  41. return err
  42. }
  43. forwardedClientIPHeaders := []string{}
  44. if s != nil && s.cfg != nil {
  45. forwardedClientIPHeaders = s.cfg.ForwardedClientIPSettings().Headers
  46. }
  47. forwardedClientIPHeadersJSON, err := json.Marshal(forwardedClientIPHeaders)
  48. if err != nil {
  49. return fmt.Errorf("marshal default forwarded client IP headers: %w", err)
  50. }
  51. // 初始化默认设置
  52. defaults := map[string]string{
  53. SettingKeyRegistrationEnabled: "true",
  54. SettingKeyEmailVerifyEnabled: "false",
  55. SettingKeyRegistrationEmailSuffixWhitelist: "[]",
  56. SettingKeyRegistrationEmailDomainQuotaEnabled: "false",
  57. SettingKeyPromoCodeEnabled: "true", // 默认启用优惠码功能
  58. SettingKeyLoginAgreementEnabled: "false",
  59. SettingKeyLoginAgreementMode: defaultLoginAgreementMode,
  60. SettingKeyLoginAgreementUpdatedAt: defaultLoginAgreementDate,
  61. SettingKeyLoginAgreementDocuments: loginAgreementDocumentsJSON,
  62. SettingKeyAPIKeyACLTrustForwardedIP: "true",
  63. SettingKeyForwardedClientIPHeaders: string(forwardedClientIPHeadersJSON),
  64. settingKeyForwardedClientIPModeV2: "true",
  65. SettingKeySiteName: "Sub2API",
  66. SettingKeySiteLogo: "",
  67. SettingKeyPurchaseSubscriptionEnabled: "false",
  68. SettingKeyPurchaseSubscriptionURL: "",
  69. SettingKeyTableDefaultPageSize: "20",
  70. SettingKeyTablePageSizeOptions: "[10,20,50,100]",
  71. SettingKeyCustomMenuItems: "[]",
  72. SettingKeyCustomEndpoints: "[]",
  73. SettingKeyWeChatConnectEnabled: "false",
  74. SettingKeyWeChatConnectAppID: "",
  75. SettingKeyWeChatConnectAppSecret: "",
  76. SettingKeyWeChatConnectOpenAppID: "",
  77. SettingKeyWeChatConnectOpenAppSecret: "",
  78. SettingKeyWeChatConnectMPAppID: "",
  79. SettingKeyWeChatConnectMPAppSecret: "",
  80. SettingKeyWeChatConnectMobileAppID: "",
  81. SettingKeyWeChatConnectMobileAppSecret: "",
  82. SettingKeyWeChatConnectOpenEnabled: "false",
  83. SettingKeyWeChatConnectMPEnabled: "false",
  84. SettingKeyWeChatConnectMobileEnabled: "false",
  85. SettingKeyWeChatConnectMode: "open",
  86. SettingKeyWeChatConnectScopes: "snsapi_login",
  87. SettingKeyWeChatConnectRedirectURL: "",
  88. SettingKeyWeChatConnectFrontendRedirectURL: defaultWeChatConnectFrontend,
  89. SettingKeyGitHubOAuthEnabled: "false",
  90. SettingKeyGitHubOAuthClientID: "",
  91. SettingKeyGitHubOAuthClientSecret: "",
  92. SettingKeyGitHubOAuthRedirectURL: "",
  93. SettingKeyGitHubOAuthFrontendRedirectURL: defaultGitHubOAuthFrontend,
  94. SettingKeyGoogleOAuthEnabled: "false",
  95. SettingKeyGoogleOAuthClientID: "",
  96. SettingKeyGoogleOAuthClientSecret: "",
  97. SettingKeyGoogleOAuthRedirectURL: "",
  98. SettingKeyGoogleOAuthFrontendRedirectURL: defaultGoogleOAuthFrontend,
  99. SettingKeyOIDCConnectEnabled: "false",
  100. SettingKeyOIDCConnectProviderName: "OIDC",
  101. SettingKeyOIDCConnectClientID: "",
  102. SettingKeyOIDCConnectClientSecret: "",
  103. SettingKeyOIDCConnectIssuerURL: "",
  104. SettingKeyOIDCConnectDiscoveryURL: "",
  105. SettingKeyOIDCConnectAuthorizeURL: "",
  106. SettingKeyOIDCConnectTokenURL: "",
  107. SettingKeyOIDCConnectUserInfoURL: "",
  108. SettingKeyOIDCConnectJWKSURL: "",
  109. SettingKeyOIDCConnectScopes: "openid email profile",
  110. SettingKeyOIDCConnectRedirectURL: "",
  111. SettingKeyOIDCConnectFrontendRedirectURL: "/auth/oidc/callback",
  112. SettingKeyOIDCConnectTokenAuthMethod: "client_secret_post",
  113. SettingKeyOIDCConnectUsePKCE: strconv.FormatBool(oidcUsePKCEDefault),
  114. SettingKeyOIDCConnectValidateIDToken: strconv.FormatBool(oidcValidateIDTokenDefault),
  115. SettingKeyOIDCConnectAllowedSigningAlgs: "RS256,ES256,PS256",
  116. SettingKeyOIDCConnectClockSkewSeconds: "120",
  117. SettingKeyOIDCConnectRequireEmailVerified: "false",
  118. SettingKeyOIDCConnectUserInfoEmailPath: "",
  119. SettingKeyOIDCConnectUserInfoIDPath: "",
  120. SettingKeyOIDCConnectUserInfoUsernamePath: "",
  121. SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
  122. SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64),
  123. SettingKeyAffiliateRebateRate: strconv.FormatFloat(AffiliateRebateRateDefault, 'f', 8, 64),
  124. SettingKeyAffiliateRebateFreezeHours: strconv.Itoa(AffiliateRebateFreezeHoursDefault),
  125. SettingKeyAffiliateRebateDurationDays: strconv.Itoa(AffiliateRebateDurationDaysDefault),
  126. SettingKeyAffiliateRebatePerInviteeCap: strconv.FormatFloat(AffiliateRebatePerInviteeCapDefault, 'f', 2, 64),
  127. SettingKeyDefaultUserRPMLimit: "0",
  128. SettingKeyDefaultSubscriptions: "[]",
  129. SettingKeyAuthSourceDefaultEmailBalance: "0",
  130. SettingKeyAuthSourceDefaultEmailConcurrency: "5",
  131. SettingKeyAuthSourceDefaultEmailSubscriptions: "[]",
  132. SettingKeyAuthSourceDefaultEmailGrantOnSignup: "false",
  133. SettingKeyAuthSourceDefaultEmailGrantOnFirstBind: "false",
  134. SettingKeyAuthSourceDefaultLinuxDoBalance: "0",
  135. SettingKeyAuthSourceDefaultLinuxDoConcurrency: "5",
  136. SettingKeyAuthSourceDefaultLinuxDoSubscriptions: "[]",
  137. SettingKeyAuthSourceDefaultLinuxDoGrantOnSignup: "false",
  138. SettingKeyAuthSourceDefaultLinuxDoGrantOnFirstBind: "false",
  139. SettingKeyAuthSourceDefaultOIDCBalance: "0",
  140. SettingKeyAuthSourceDefaultOIDCConcurrency: "5",
  141. SettingKeyAuthSourceDefaultOIDCSubscriptions: "[]",
  142. SettingKeyAuthSourceDefaultOIDCGrantOnSignup: "false",
  143. SettingKeyAuthSourceDefaultOIDCGrantOnFirstBind: "false",
  144. SettingKeyAuthSourceDefaultWeChatBalance: "0",
  145. SettingKeyAuthSourceDefaultWeChatConcurrency: "5",
  146. SettingKeyAuthSourceDefaultWeChatSubscriptions: "[]",
  147. SettingKeyAuthSourceDefaultWeChatGrantOnSignup: "false",
  148. SettingKeyAuthSourceDefaultWeChatGrantOnFirstBind: "false",
  149. SettingKeyAuthSourceDefaultGitHubBalance: "0",
  150. SettingKeyAuthSourceDefaultGitHubConcurrency: "5",
  151. SettingKeyAuthSourceDefaultGitHubSubscriptions: "[]",
  152. SettingKeyAuthSourceDefaultGitHubGrantOnSignup: "false",
  153. SettingKeyAuthSourceDefaultGitHubGrantOnFirstBind: "false",
  154. SettingKeyAuthSourceDefaultGoogleBalance: "0",
  155. SettingKeyAuthSourceDefaultGoogleConcurrency: "5",
  156. SettingKeyAuthSourceDefaultGoogleSubscriptions: "[]",
  157. SettingKeyAuthSourceDefaultGoogleGrantOnSignup: "false",
  158. SettingKeyAuthSourceDefaultGoogleGrantOnFirstBind: "false",
  159. SettingKeyAuthSourceDefaultDingTalkBalance: "0",
  160. SettingKeyAuthSourceDefaultDingTalkConcurrency: "5",
  161. SettingKeyAuthSourceDefaultDingTalkSubscriptions: "[]",
  162. SettingKeyAuthSourceDefaultDingTalkGrantOnSignup: "false",
  163. SettingKeyAuthSourceDefaultDingTalkGrantOnFirstBind: "false",
  164. SettingKeyForceEmailOnThirdPartySignup: "false",
  165. SettingKeySMTPPort: "587",
  166. SettingKeySMTPUseTLS: "false",
  167. // Model fallback defaults
  168. SettingKeyEnableModelFallback: "false",
  169. SettingKeyFallbackModelAnthropic: "claude-3-5-sonnet-20241022",
  170. SettingKeyFallbackModelOpenAI: "gpt-4o",
  171. SettingKeyFallbackModelGemini: "gemini-2.5-pro",
  172. SettingKeyFallbackModelAntigravity: "gemini-2.5-pro",
  173. // Identity patch defaults
  174. SettingKeyEnableIdentityPatch: "true",
  175. SettingKeyIdentityPatchPrompt: "",
  176. // Ops monitoring defaults (vNext)
  177. SettingKeyOpsMonitoringEnabled: "true",
  178. SettingKeyOpsRealtimeMonitoringEnabled: "true",
  179. SettingKeyOpsQueryModeDefault: "auto",
  180. SettingKeyOpsMetricsIntervalSeconds: "60",
  181. // Channel monitor defaults (enabled, 60s)
  182. SettingKeyChannelMonitorEnabled: "true",
  183. SettingKeyChannelMonitorMode: ChannelMonitorModeV1,
  184. SettingKeyChannelMonitorDefaultIntervalSeconds: "60",
  185. SettingKeyChannelMonitorHideThroughput: "true",
  186. SettingKeyChannelMonitorShowQuota: "false",
  187. SettingKeyChannelMonitorHideUserRanking: "false",
  188. // Grok compatibility defaults: cross-client mapping stays enabled unless
  189. // operators explicitly disable it.
  190. SettingKeyGrokDefaultTextModel: "grok-4.6",
  191. SettingKeyGrokCrossClientModelMapEnabled: "true",
  192. SettingKeyGrokDefaultBaseURLMode: GrokDefaultBaseURLModeCLI,
  193. // Available channels feature (default disabled; opt-in)
  194. SettingKeyAvailableChannelsEnabled: "false",
  195. // Model plaza feature (default disabled; opt-in, public unless require_auth)
  196. SettingKeyModelPlazaEnabled: "false",
  197. SettingKeyModelPlazaRequireAuth: "false",
  198. SettingKeyModelPlazaDescription: "",
  199. SettingKeyPluginManagementEnabled: "false",
  200. // Affiliate (邀请返利) feature (default disabled; opt-in)
  201. SettingKeyAffiliateEnabled: "false",
  202. SettingKeyAffiliateAdminRechargeEnabled: strconv.FormatBool(AdminRechargeRebateEnabledDefault),
  203. // 风控中心功能(默认关闭,显式启用)
  204. SettingKeyRiskControlEnabled: "false",
  205. // cyber 会话屏蔽(默认关闭,TTL 默认 3600s)
  206. SettingKeyCyberSessionBlockEnabled: "false",
  207. SettingKeyCyberSessionBlockTTLSeconds: "3600",
  208. // Claude Code version check (default: empty = disabled)
  209. SettingKeyMinClaudeCodeVersion: "",
  210. SettingKeyMaxClaudeCodeVersion: "",
  211. // codex_cli_only 加固(默认:版本不检查、名单空、默认种子指纹信号)
  212. SettingKeyMinCodexVersion: "",
  213. SettingKeyMaxCodexVersion: "",
  214. SettingKeyCodexCLIOnlyBlacklist: "",
  215. SettingKeyCodexCLIOnlyWhitelist: "",
  216. SettingKeyCodexCLIOnlyAllowAppServerClients: "false",
  217. SettingKeyCodexCLIOnlyEngineFingerprintSignals: openai.DefaultEngineFingerprintSignalsJSON(),
  218. // 分组隔离(默认不允许未分组 Key 调度)
  219. SettingKeyAllowUngroupedKeyScheduling: "false",
  220. SettingKeyOpenAILowUpstreamRatePriorityEnabled: "false",
  221. SettingKeyOpenAIOAuthSchedulingRateMultiplier: "1",
  222. SettingKeyEnableAnthropicCacheTTL1hInjection: "false",
  223. SettingKeyRewriteMessageCacheControl: strconv.FormatBool(s.defaultRewriteMessageCacheControl()),
  224. SettingKeyEnableClientDatelineNormalization: "true",
  225. SettingKeyAntigravityUserAgentVersion: "",
  226. SettingKeyOpenAICodexUserAgent: "",
  227. SettingKeyOpenAICodexClientVersion: "",
  228. SettingKeyOpenAICodexClientVersionSynced: "",
  229. SettingKeyOpenAICodexVersionAutoSyncEnabled: "true",
  230. SettingPaymentVisibleMethodAlipaySource: "",
  231. SettingPaymentVisibleMethodWxpaySource: "",
  232. SettingPaymentVisibleMethodAlipayEnabled: "false",
  233. SettingPaymentVisibleMethodWxpayEnabled: "false",
  234. openAIAdvancedSchedulerSettingKey: "false",
  235. SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled: "false",
  236. SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled: "false",
  237. SettingKeyOpenAIAdvancedSchedulerLBTopK: "",
  238. SettingKeyOpenAIAdvancedSchedulerWeightPriority: "",
  239. SettingKeyOpenAIAdvancedSchedulerWeightLoad: "",
  240. SettingKeyOpenAIAdvancedSchedulerWeightQueue: "",
  241. SettingKeyOpenAIAdvancedSchedulerWeightErrorRate: "",
  242. SettingKeyOpenAIAdvancedSchedulerWeightTTFT: "",
  243. SettingKeyOpenAIAdvancedSchedulerWeightReset: "",
  244. SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom: "",
  245. SettingKeyOpenAIAdvancedSchedulerWeightUpstreamCost: "",
  246. SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse: "",
  247. SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky: "",
  248. SettingKeyAllowUserViewErrorRequests: "false",
  249. }
  250. return s.settingRepo.SetMultiple(ctx, defaults)
  251. }
  252. func parseForwardedClientIPHeadersSetting(value string) ([]string, error) {
  253. var headers []string
  254. if err := json.Unmarshal([]byte(value), &headers); err != nil {
  255. return nil, fmt.Errorf("parse forwarded_client_ip_headers: %w", err)
  256. }
  257. if headers == nil {
  258. return nil, fmt.Errorf("parse forwarded_client_ip_headers: value must be a JSON array")
  259. }
  260. normalized, err := config.NormalizeForwardedClientIPHeaders(headers)
  261. if err != nil {
  262. return nil, fmt.Errorf("parse forwarded_client_ip_headers: %w", err)
  263. }
  264. return normalized, nil
  265. }
  266. // parseSettings 解析设置到结构体
  267. func (s *SettingService) parseSettings(settings map[string]string) *SystemSettings {
  268. emailVerifyEnabled := settings[SettingKeyEmailVerifyEnabled] == "true"
  269. loginAgreementDocuments := parseLoginAgreementDocuments(settings[SettingKeyLoginAgreementDocuments])
  270. loginAgreementUpdatedAt := strings.TrimSpace(settings[SettingKeyLoginAgreementUpdatedAt])
  271. if loginAgreementUpdatedAt == "" {
  272. loginAgreementUpdatedAt = defaultLoginAgreementDate
  273. }
  274. apiKeyACLTrustForwardedIP := false
  275. forwardedClientIPHeaders := []string{}
  276. if s != nil && s.cfg != nil {
  277. runtimeSettings := s.cfg.ForwardedClientIPSettings()
  278. apiKeyACLTrustForwardedIP = runtimeSettings.TrustForwardedIP
  279. forwardedClientIPHeaders = runtimeSettings.Headers
  280. }
  281. if value, ok := settings[SettingKeyAPIKeyACLTrustForwardedIP]; ok {
  282. apiKeyACLTrustForwardedIP = value == "true"
  283. }
  284. if value, ok := settings[SettingKeyForwardedClientIPHeaders]; ok {
  285. parsed, err := parseForwardedClientIPHeadersSetting(value)
  286. if err != nil {
  287. slog.Error("invalid persisted forwarded client IP headers; forwarded trust disabled", "error", err)
  288. apiKeyACLTrustForwardedIP = false
  289. forwardedClientIPHeaders = []string{}
  290. } else {
  291. forwardedClientIPHeaders = parsed
  292. }
  293. }
  294. result := &SystemSettings{
  295. RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
  296. EmailVerifyEnabled: emailVerifyEnabled,
  297. RegistrationEmailSuffixWhitelist: ParseRegistrationEmailSuffixWhitelist(settings[SettingKeyRegistrationEmailSuffixWhitelist]),
  298. RegistrationEmailDomainQuotaEnabled: settings[SettingKeyRegistrationEmailDomainQuotaEnabled] == "true",
  299. PromoCodeEnabled: settings[SettingKeyPromoCodeEnabled] != "false", // 默认启用
  300. PasswordResetEnabled: emailVerifyEnabled && settings[SettingKeyPasswordResetEnabled] == "true",
  301. FrontendURL: settings[SettingKeyFrontendURL],
  302. InvitationCodeEnabled: settings[SettingKeyInvitationCodeEnabled] == "true",
  303. TotpEnabled: settings[SettingKeyTotpEnabled] == "true",
  304. PasskeyEnabled: s.passkeySettingEnabled(settings),
  305. SessionBindingEnabled: settings[SettingKeySessionBindingEnabled] == "true", // 默认关闭
  306. StepUpEnabled: settings[SettingKeyStepUpEnabled] == "true", // 默认关闭
  307. AuditLogRetentionDays: parseAuditLogRetentionDays(settings[SettingKeyAuditLogRetentionDays]),
  308. LoginAgreementEnabled: settings[SettingKeyLoginAgreementEnabled] == "true",
  309. LoginAgreementMode: normalizeLoginAgreementMode(settings[SettingKeyLoginAgreementMode]),
  310. LoginAgreementUpdatedAt: loginAgreementUpdatedAt,
  311. LoginAgreementDocuments: loginAgreementDocuments,
  312. SMTPHost: settings[SettingKeySMTPHost],
  313. SMTPUsername: settings[SettingKeySMTPUsername],
  314. SMTPFrom: settings[SettingKeySMTPFrom],
  315. SMTPFromName: settings[SettingKeySMTPFromName],
  316. SMTPUseTLS: settings[SettingKeySMTPUseTLS] == "true",
  317. SMTPPasswordConfigured: settings[SettingKeySMTPPassword] != "",
  318. TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true",
  319. TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
  320. TurnstileSecretKeyConfigured: settings[SettingKeyTurnstileSecretKey] != "",
  321. TencentCaptchaEnabled: settings[SettingKeyTencentCaptchaEnabled] == "true",
  322. TencentCaptchaAppID: settings[SettingKeyTencentCaptchaAppID],
  323. TencentCaptchaAppSecretKeyConfigured: settings[SettingKeyTencentCaptchaAppSecretKey] != "",
  324. TencentCaptchaCloudSecretIDConfigured: settings[SettingKeyTencentCaptchaCloudSecretID] != "",
  325. TencentCaptchaCloudSecretKeyConfigured: settings[SettingKeyTencentCaptchaCloudSecretKey] != "",
  326. TencentCaptchaRegion: normalizeTencentCaptchaRegion(settings[SettingKeyTencentCaptchaRegion]),
  327. AliyunCaptchaEnabled: settings[SettingKeyAliyunCaptchaEnabled] == "true",
  328. AliyunCaptchaAccessKeyID: settings[SettingKeyAliyunCaptchaAccessKeyID],
  329. AliyunCaptchaAccessKeySecretConfigured: settings[SettingKeyAliyunCaptchaAccessKeySecret] != "",
  330. AliyunCaptchaSceneID: settings[SettingKeyAliyunCaptchaSceneID],
  331. AliyunCaptchaPrefix: settings[SettingKeyAliyunCaptchaPrefix],
  332. AliyunCaptchaRegion: normalizeAliyunCaptchaRegion(settings[SettingKeyAliyunCaptchaRegion]),
  333. APIKeyACLTrustForwardedIP: apiKeyACLTrustForwardedIP,
  334. ForwardedClientIPHeaders: forwardedClientIPHeaders,
  335. SiteName: s.getStringOrDefault(settings, SettingKeySiteName, "Sub2API"),
  336. SiteLogo: settings[SettingKeySiteLogo],
  337. SiteSubtitle: s.getStringOrDefault(settings, SettingKeySiteSubtitle, "Subscription to API Conversion Platform"),
  338. APIBaseURL: settings[SettingKeyAPIBaseURL],
  339. ContactInfo: settings[SettingKeyContactInfo],
  340. DocURL: settings[SettingKeyDocURL],
  341. HomeContent: settings[SettingKeyHomeContent],
  342. CompactHomeEnabled: settings[SettingKeyCompactHomeEnabled] == "true",
  343. HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
  344. PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true",
  345. PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]),
  346. CustomMenuItems: settings[SettingKeyCustomMenuItems],
  347. CustomEndpoints: settings[SettingKeyCustomEndpoints],
  348. BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true",
  349. }
  350. result.TableDefaultPageSize, result.TablePageSizeOptions = parseTablePreferences(
  351. settings[SettingKeyTableDefaultPageSize],
  352. settings[SettingKeyTablePageSizeOptions],
  353. )
  354. // 解析整数类型
  355. if port, err := strconv.Atoi(settings[SettingKeySMTPPort]); err == nil {
  356. result.SMTPPort = port
  357. } else {
  358. result.SMTPPort = 587
  359. }
  360. if concurrency, err := strconv.Atoi(settings[SettingKeyDefaultConcurrency]); err == nil {
  361. result.DefaultConcurrency = concurrency
  362. } else {
  363. result.DefaultConcurrency = s.cfg.Default.UserConcurrency
  364. }
  365. if rpm, err := strconv.Atoi(settings[SettingKeyDefaultUserRPMLimit]); err == nil && rpm >= 0 {
  366. result.DefaultUserRPMLimit = rpm
  367. }
  368. // 解析浮点数类型
  369. if balance, err := strconv.ParseFloat(settings[SettingKeyDefaultBalance], 64); err == nil {
  370. result.DefaultBalance = balance
  371. } else {
  372. result.DefaultBalance = s.cfg.Default.UserBalance
  373. }
  374. if rebateRate, err := strconv.ParseFloat(settings[SettingKeyAffiliateRebateRate], 64); err == nil {
  375. result.AffiliateRebateRate = clampAffiliateRebateRate(rebateRate)
  376. } else {
  377. result.AffiliateRebateRate = AffiliateRebateRateDefault
  378. }
  379. if freezeHours, err := strconv.Atoi(settings[SettingKeyAffiliateRebateFreezeHours]); err == nil && freezeHours >= 0 {
  380. if freezeHours > AffiliateRebateFreezeHoursMax {
  381. freezeHours = AffiliateRebateFreezeHoursMax
  382. }
  383. result.AffiliateRebateFreezeHours = freezeHours
  384. }
  385. if durationDays, err := strconv.Atoi(settings[SettingKeyAffiliateRebateDurationDays]); err == nil && durationDays >= 0 {
  386. if durationDays > AffiliateRebateDurationDaysMax {
  387. durationDays = AffiliateRebateDurationDaysMax
  388. }
  389. result.AffiliateRebateDurationDays = durationDays
  390. }
  391. if perInviteeCap, err := strconv.ParseFloat(settings[SettingKeyAffiliateRebatePerInviteeCap], 64); err == nil && perInviteeCap >= 0 {
  392. result.AffiliateRebatePerInviteeCap = perInviteeCap
  393. }
  394. result.AdminRechargeRebateEnabled = settings[SettingKeyAffiliateAdminRechargeEnabled] == "true"
  395. result.DefaultSubscriptions = parseDefaultSubscriptions(settings[SettingKeyDefaultSubscriptions])
  396. // 敏感信息直接返回,方便测试连接时使用
  397. result.SMTPPassword = settings[SettingKeySMTPPassword]
  398. result.TurnstileSecretKey = settings[SettingKeyTurnstileSecretKey]
  399. result.TencentCaptchaAppSecretKey = settings[SettingKeyTencentCaptchaAppSecretKey]
  400. result.TencentCaptchaCloudSecretID = settings[SettingKeyTencentCaptchaCloudSecretID]
  401. result.TencentCaptchaCloudSecretKey = settings[SettingKeyTencentCaptchaCloudSecretKey]
  402. result.AliyunCaptchaAccessKeySecret = settings[SettingKeyAliyunCaptchaAccessKeySecret]
  403. // LinuxDo Connect 设置:
  404. // - 兼容 config.yaml/env(避免老部署因为未迁移到数据库设置而被意外关闭)
  405. // - 支持在后台“系统设置”中覆盖并持久化(存储于 DB)
  406. linuxDoBase := config.LinuxDoConnectConfig{}
  407. if s.cfg != nil {
  408. linuxDoBase = s.cfg.LinuxDo
  409. }
  410. if raw, ok := settings[SettingKeyLinuxDoConnectEnabled]; ok {
  411. result.LinuxDoConnectEnabled = raw == "true"
  412. } else {
  413. result.LinuxDoConnectEnabled = linuxDoBase.Enabled
  414. }
  415. if v, ok := settings[SettingKeyLinuxDoConnectClientID]; ok && strings.TrimSpace(v) != "" {
  416. result.LinuxDoConnectClientID = strings.TrimSpace(v)
  417. } else {
  418. result.LinuxDoConnectClientID = linuxDoBase.ClientID
  419. }
  420. if v, ok := settings[SettingKeyLinuxDoConnectRedirectURL]; ok && strings.TrimSpace(v) != "" {
  421. result.LinuxDoConnectRedirectURL = strings.TrimSpace(v)
  422. } else {
  423. result.LinuxDoConnectRedirectURL = linuxDoBase.RedirectURL
  424. }
  425. result.LinuxDoConnectClientSecret = strings.TrimSpace(settings[SettingKeyLinuxDoConnectClientSecret])
  426. if result.LinuxDoConnectClientSecret == "" {
  427. result.LinuxDoConnectClientSecret = strings.TrimSpace(linuxDoBase.ClientSecret)
  428. }
  429. result.LinuxDoConnectClientSecretConfigured = result.LinuxDoConnectClientSecret != ""
  430. // DingTalk Connect 设置:
  431. // - 兼容 config.yaml/env
  432. // - 支持后台系统设置覆盖并持久化(存储于 DB)
  433. dingTalkBase := config.DingTalkConnectConfig{}
  434. if s.cfg != nil {
  435. dingTalkBase = s.cfg.DingTalk
  436. }
  437. if raw, ok := settings[SettingKeyDingTalkConnectEnabled]; ok {
  438. result.DingTalkConnectEnabled = raw == "true"
  439. } else {
  440. result.DingTalkConnectEnabled = dingTalkBase.Enabled
  441. }
  442. if v, ok := settings[SettingKeyDingTalkConnectClientID]; ok && strings.TrimSpace(v) != "" {
  443. result.DingTalkConnectClientID = strings.TrimSpace(v)
  444. } else {
  445. result.DingTalkConnectClientID = dingTalkBase.ClientID
  446. }
  447. if v, ok := settings[SettingKeyDingTalkConnectRedirectURL]; ok && strings.TrimSpace(v) != "" {
  448. result.DingTalkConnectRedirectURL = strings.TrimSpace(v)
  449. } else {
  450. result.DingTalkConnectRedirectURL = dingTalkBase.RedirectURL
  451. }
  452. result.DingTalkConnectClientSecret = strings.TrimSpace(settings[SettingKeyDingTalkConnectClientSecret])
  453. if result.DingTalkConnectClientSecret == "" {
  454. result.DingTalkConnectClientSecret = strings.TrimSpace(dingTalkBase.ClientSecret)
  455. }
  456. result.DingTalkConnectClientSecretConfigured = result.DingTalkConnectClientSecret != ""
  457. if v, ok := settings[SettingKeyDingTalkConnectCorpRestrictionPolicy]; ok && strings.TrimSpace(v) != "" {
  458. result.DingTalkConnectCorpRestrictionPolicy = strings.TrimSpace(v)
  459. } else {
  460. result.DingTalkConnectCorpRestrictionPolicy = dingTalkBase.CorpRestrictionPolicy
  461. }
  462. result.DingTalkConnectCorpRestrictionPolicy = coerceDeprecatedDingTalkCorpPolicy(result.DingTalkConnectCorpRestrictionPolicy)
  463. if v, ok := settings[SettingKeyDingTalkConnectInternalCorpID]; ok && strings.TrimSpace(v) != "" {
  464. result.DingTalkConnectInternalCorpID = strings.TrimSpace(v)
  465. } else {
  466. result.DingTalkConnectInternalCorpID = dingTalkBase.InternalCorpID
  467. }
  468. if v, ok := settings[SettingKeyDingTalkConnectBypassRegistration]; ok && strings.TrimSpace(v) != "" {
  469. result.DingTalkConnectBypassRegistration = strings.EqualFold(strings.TrimSpace(v), "true")
  470. } else {
  471. result.DingTalkConnectBypassRegistration = dingTalkBase.BypassRegistration
  472. }
  473. // bypass_registration 仅在 internal_only 模式下有意义;其它策略下强制 false,
  474. // 以保证加载出的 effective config 永远是一致状态。
  475. if result.DingTalkConnectCorpRestrictionPolicy != "internal_only" {
  476. result.DingTalkConnectBypassRegistration = false
  477. }
  478. if v, ok := settings[SettingKeyDingTalkConnectSyncCorpEmail]; ok && strings.TrimSpace(v) != "" {
  479. result.DingTalkConnectSyncCorpEmail = strings.EqualFold(strings.TrimSpace(v), "true")
  480. } else {
  481. result.DingTalkConnectSyncCorpEmail = dingTalkBase.SyncCorpEmail
  482. }
  483. if v, ok := settings[SettingKeyDingTalkConnectSyncDisplayName]; ok && strings.TrimSpace(v) != "" {
  484. result.DingTalkConnectSyncDisplayName = strings.EqualFold(strings.TrimSpace(v), "true")
  485. } else {
  486. result.DingTalkConnectSyncDisplayName = dingTalkBase.SyncDisplayName
  487. }
  488. if v, ok := settings[SettingKeyDingTalkConnectSyncDept]; ok && strings.TrimSpace(v) != "" {
  489. result.DingTalkConnectSyncDept = strings.EqualFold(strings.TrimSpace(v), "true")
  490. } else {
  491. result.DingTalkConnectSyncDept = dingTalkBase.SyncDept
  492. }
  493. // 身份同步三开关仅在 internal_only 模式下有意义;其它策略强制 false。
  494. if result.DingTalkConnectCorpRestrictionPolicy != "internal_only" {
  495. result.DingTalkConnectSyncCorpEmail = false
  496. result.DingTalkConnectSyncDisplayName = false
  497. result.DingTalkConnectSyncDept = false
  498. }
  499. // 身份同步目标 attr key(DB 空 → fallback 默认值)
  500. result.DingTalkConnectSyncCorpEmailAttrKey = strings.TrimSpace(settings[SettingKeyDingTalkConnectSyncCorpEmailAttrKey])
  501. if result.DingTalkConnectSyncCorpEmailAttrKey == "" {
  502. if v := strings.TrimSpace(dingTalkBase.SyncCorpEmailAttrKey); v != "" {
  503. result.DingTalkConnectSyncCorpEmailAttrKey = v
  504. } else {
  505. result.DingTalkConnectSyncCorpEmailAttrKey = "dingtalk_email"
  506. }
  507. }
  508. result.DingTalkConnectSyncDisplayNameAttrKey = strings.TrimSpace(settings[SettingKeyDingTalkConnectSyncDisplayNameAttrKey])
  509. if result.DingTalkConnectSyncDisplayNameAttrKey == "" {
  510. if v := strings.TrimSpace(dingTalkBase.SyncDisplayNameAttrKey); v != "" {
  511. result.DingTalkConnectSyncDisplayNameAttrKey = v
  512. } else {
  513. result.DingTalkConnectSyncDisplayNameAttrKey = "dingtalk_name"
  514. }
  515. }
  516. result.DingTalkConnectSyncDeptAttrKey = strings.TrimSpace(settings[SettingKeyDingTalkConnectSyncDeptAttrKey])
  517. if result.DingTalkConnectSyncDeptAttrKey == "" {
  518. if v := strings.TrimSpace(dingTalkBase.SyncDeptAttrKey); v != "" {
  519. result.DingTalkConnectSyncDeptAttrKey = v
  520. } else {
  521. result.DingTalkConnectSyncDeptAttrKey = "dingtalk_department"
  522. }
  523. }
  524. // 身份同步目标 attr 显示名称(DB 空 → fallback 默认中文)
  525. result.DingTalkConnectSyncCorpEmailAttrName = strings.TrimSpace(settings[SettingKeyDingTalkConnectSyncCorpEmailAttrName])
  526. if result.DingTalkConnectSyncCorpEmailAttrName == "" {
  527. if v := strings.TrimSpace(dingTalkBase.SyncCorpEmailAttrName); v != "" {
  528. result.DingTalkConnectSyncCorpEmailAttrName = v
  529. } else {
  530. result.DingTalkConnectSyncCorpEmailAttrName = "钉钉企业邮箱"
  531. }
  532. }
  533. result.DingTalkConnectSyncDisplayNameAttrName = strings.TrimSpace(settings[SettingKeyDingTalkConnectSyncDisplayNameAttrName])
  534. if result.DingTalkConnectSyncDisplayNameAttrName == "" {
  535. if v := strings.TrimSpace(dingTalkBase.SyncDisplayNameAttrName); v != "" {
  536. result.DingTalkConnectSyncDisplayNameAttrName = v
  537. } else {
  538. result.DingTalkConnectSyncDisplayNameAttrName = "钉钉姓名"
  539. }
  540. }
  541. result.DingTalkConnectSyncDeptAttrName = strings.TrimSpace(settings[SettingKeyDingTalkConnectSyncDeptAttrName])
  542. if result.DingTalkConnectSyncDeptAttrName == "" {
  543. if v := strings.TrimSpace(dingTalkBase.SyncDeptAttrName); v != "" {
  544. result.DingTalkConnectSyncDeptAttrName = v
  545. } else {
  546. result.DingTalkConnectSyncDeptAttrName = "钉钉部门"
  547. }
  548. }
  549. // Generic OIDC 设置:
  550. // - 兼容 config.yaml/env
  551. // - 支持后台系统设置覆盖并持久化(存储于 DB)
  552. oidcBase := config.OIDCConnectConfig{}
  553. if s.cfg != nil {
  554. oidcBase = s.cfg.OIDC
  555. }
  556. if raw, ok := settings[SettingKeyOIDCConnectEnabled]; ok {
  557. result.OIDCConnectEnabled = raw == "true"
  558. } else {
  559. result.OIDCConnectEnabled = oidcBase.Enabled
  560. }
  561. if v, ok := settings[SettingKeyOIDCConnectProviderName]; ok && strings.TrimSpace(v) != "" {
  562. result.OIDCConnectProviderName = strings.TrimSpace(v)
  563. } else {
  564. result.OIDCConnectProviderName = strings.TrimSpace(oidcBase.ProviderName)
  565. }
  566. if result.OIDCConnectProviderName == "" {
  567. result.OIDCConnectProviderName = "OIDC"
  568. }
  569. if v, ok := settings[SettingKeyOIDCConnectClientID]; ok && strings.TrimSpace(v) != "" {
  570. result.OIDCConnectClientID = strings.TrimSpace(v)
  571. } else {
  572. result.OIDCConnectClientID = strings.TrimSpace(oidcBase.ClientID)
  573. }
  574. if v, ok := settings[SettingKeyOIDCConnectIssuerURL]; ok && strings.TrimSpace(v) != "" {
  575. result.OIDCConnectIssuerURL = strings.TrimSpace(v)
  576. } else {
  577. result.OIDCConnectIssuerURL = strings.TrimSpace(oidcBase.IssuerURL)
  578. }
  579. if v, ok := settings[SettingKeyOIDCConnectDiscoveryURL]; ok && strings.TrimSpace(v) != "" {
  580. result.OIDCConnectDiscoveryURL = strings.TrimSpace(v)
  581. } else {
  582. result.OIDCConnectDiscoveryURL = strings.TrimSpace(oidcBase.DiscoveryURL)
  583. }
  584. if v, ok := settings[SettingKeyOIDCConnectAuthorizeURL]; ok && strings.TrimSpace(v) != "" {
  585. result.OIDCConnectAuthorizeURL = strings.TrimSpace(v)
  586. } else {
  587. result.OIDCConnectAuthorizeURL = strings.TrimSpace(oidcBase.AuthorizeURL)
  588. }
  589. if v, ok := settings[SettingKeyOIDCConnectTokenURL]; ok && strings.TrimSpace(v) != "" {
  590. result.OIDCConnectTokenURL = strings.TrimSpace(v)
  591. } else {
  592. result.OIDCConnectTokenURL = strings.TrimSpace(oidcBase.TokenURL)
  593. }
  594. if v, ok := settings[SettingKeyOIDCConnectUserInfoURL]; ok && strings.TrimSpace(v) != "" {
  595. result.OIDCConnectUserInfoURL = strings.TrimSpace(v)
  596. } else {
  597. result.OIDCConnectUserInfoURL = strings.TrimSpace(oidcBase.UserInfoURL)
  598. }
  599. if v, ok := settings[SettingKeyOIDCConnectJWKSURL]; ok && strings.TrimSpace(v) != "" {
  600. result.OIDCConnectJWKSURL = strings.TrimSpace(v)
  601. } else {
  602. result.OIDCConnectJWKSURL = strings.TrimSpace(oidcBase.JWKSURL)
  603. }
  604. if v, ok := settings[SettingKeyOIDCConnectScopes]; ok && strings.TrimSpace(v) != "" {
  605. result.OIDCConnectScopes = strings.TrimSpace(v)
  606. } else {
  607. result.OIDCConnectScopes = strings.TrimSpace(oidcBase.Scopes)
  608. }
  609. if v, ok := settings[SettingKeyOIDCConnectRedirectURL]; ok && strings.TrimSpace(v) != "" {
  610. result.OIDCConnectRedirectURL = strings.TrimSpace(v)
  611. } else {
  612. result.OIDCConnectRedirectURL = strings.TrimSpace(oidcBase.RedirectURL)
  613. }
  614. if v, ok := settings[SettingKeyOIDCConnectFrontendRedirectURL]; ok && strings.TrimSpace(v) != "" {
  615. result.OIDCConnectFrontendRedirectURL = strings.TrimSpace(v)
  616. } else {
  617. result.OIDCConnectFrontendRedirectURL = strings.TrimSpace(oidcBase.FrontendRedirectURL)
  618. }
  619. if v, ok := settings[SettingKeyOIDCConnectTokenAuthMethod]; ok && strings.TrimSpace(v) != "" {
  620. result.OIDCConnectTokenAuthMethod = strings.ToLower(strings.TrimSpace(v))
  621. } else {
  622. result.OIDCConnectTokenAuthMethod = strings.ToLower(strings.TrimSpace(oidcBase.TokenAuthMethod))
  623. }
  624. if raw, ok := settings[SettingKeyOIDCConnectUsePKCE]; ok {
  625. result.OIDCConnectUsePKCE = raw == "true"
  626. } else {
  627. result.OIDCConnectUsePKCE = oidcUsePKCECompatibilityDefault(oidcBase)
  628. }
  629. if raw, ok := settings[SettingKeyOIDCConnectValidateIDToken]; ok {
  630. result.OIDCConnectValidateIDToken = raw == "true"
  631. } else {
  632. result.OIDCConnectValidateIDToken = oidcValidateIDTokenCompatibilityDefault(oidcBase)
  633. }
  634. if v, ok := settings[SettingKeyOIDCConnectAllowedSigningAlgs]; ok && strings.TrimSpace(v) != "" {
  635. result.OIDCConnectAllowedSigningAlgs = strings.TrimSpace(v)
  636. } else {
  637. result.OIDCConnectAllowedSigningAlgs = strings.TrimSpace(oidcBase.AllowedSigningAlgs)
  638. }
  639. clockSkewSet := false
  640. if raw, ok := settings[SettingKeyOIDCConnectClockSkewSeconds]; ok && strings.TrimSpace(raw) != "" {
  641. if parsed, err := strconv.Atoi(strings.TrimSpace(raw)); err == nil {
  642. result.OIDCConnectClockSkewSeconds = parsed
  643. clockSkewSet = true
  644. }
  645. }
  646. if !clockSkewSet {
  647. result.OIDCConnectClockSkewSeconds = oidcBase.ClockSkewSeconds
  648. }
  649. if !clockSkewSet && result.OIDCConnectClockSkewSeconds == 0 {
  650. result.OIDCConnectClockSkewSeconds = 120
  651. }
  652. if raw, ok := settings[SettingKeyOIDCConnectRequireEmailVerified]; ok {
  653. result.OIDCConnectRequireEmailVerified = raw == "true"
  654. } else {
  655. result.OIDCConnectRequireEmailVerified = oidcBase.RequireEmailVerified
  656. }
  657. if v, ok := settings[SettingKeyOIDCConnectUserInfoEmailPath]; ok {
  658. result.OIDCConnectUserInfoEmailPath = strings.TrimSpace(v)
  659. } else {
  660. result.OIDCConnectUserInfoEmailPath = strings.TrimSpace(oidcBase.UserInfoEmailPath)
  661. }
  662. if v, ok := settings[SettingKeyOIDCConnectUserInfoIDPath]; ok {
  663. result.OIDCConnectUserInfoIDPath = strings.TrimSpace(v)
  664. } else {
  665. result.OIDCConnectUserInfoIDPath = strings.TrimSpace(oidcBase.UserInfoIDPath)
  666. }
  667. if v, ok := settings[SettingKeyOIDCConnectUserInfoUsernamePath]; ok {
  668. result.OIDCConnectUserInfoUsernamePath = strings.TrimSpace(v)
  669. } else {
  670. result.OIDCConnectUserInfoUsernamePath = strings.TrimSpace(oidcBase.UserInfoUsernamePath)
  671. }
  672. result.OIDCConnectClientSecret = strings.TrimSpace(settings[SettingKeyOIDCConnectClientSecret])
  673. if result.OIDCConnectClientSecret == "" {
  674. result.OIDCConnectClientSecret = strings.TrimSpace(oidcBase.ClientSecret)
  675. }
  676. result.OIDCConnectClientSecretConfigured = result.OIDCConnectClientSecret != ""
  677. gitHubEffective := s.effectiveEmailOAuthConfig(settings, "github")
  678. result.GitHubOAuthEnabled = gitHubEffective.Enabled
  679. result.GitHubOAuthClientID = strings.TrimSpace(gitHubEffective.ClientID)
  680. result.GitHubOAuthClientSecret = strings.TrimSpace(gitHubEffective.ClientSecret)
  681. result.GitHubOAuthClientSecretConfigured = result.GitHubOAuthClientSecret != ""
  682. result.GitHubOAuthRedirectURL = strings.TrimSpace(gitHubEffective.RedirectURL)
  683. result.GitHubOAuthFrontendRedirectURL = strings.TrimSpace(gitHubEffective.FrontendRedirectURL)
  684. googleEffective := s.effectiveEmailOAuthConfig(settings, "google")
  685. result.GoogleOAuthEnabled = googleEffective.Enabled
  686. result.GoogleOAuthClientID = strings.TrimSpace(googleEffective.ClientID)
  687. result.GoogleOAuthClientSecret = strings.TrimSpace(googleEffective.ClientSecret)
  688. result.GoogleOAuthClientSecretConfigured = result.GoogleOAuthClientSecret != ""
  689. result.GoogleOAuthRedirectURL = strings.TrimSpace(googleEffective.RedirectURL)
  690. result.GoogleOAuthFrontendRedirectURL = strings.TrimSpace(googleEffective.FrontendRedirectURL)
  691. // WeChat Connect 设置:
  692. // - 优先读取 DB 系统设置
  693. // - 缺失时回退到 config/env,保持升级兼容
  694. weChatEffective := s.effectiveWeChatConnectOAuthConfig(settings)
  695. result.WeChatConnectEnabled = weChatEffective.Enabled
  696. result.WeChatConnectAppID = weChatEffective.LegacyAppID
  697. result.WeChatConnectAppSecret = weChatEffective.LegacyAppSecret
  698. result.WeChatConnectAppSecretConfigured = weChatEffective.LegacyAppSecret != ""
  699. result.WeChatConnectOpenAppID = weChatEffective.OpenAppID
  700. result.WeChatConnectOpenAppSecret = weChatEffective.OpenAppSecret
  701. result.WeChatConnectOpenAppSecretConfigured = weChatEffective.OpenAppSecret != ""
  702. result.WeChatConnectMPAppID = weChatEffective.MPAppID
  703. result.WeChatConnectMPAppSecret = weChatEffective.MPAppSecret
  704. result.WeChatConnectMPAppSecretConfigured = weChatEffective.MPAppSecret != ""
  705. result.WeChatConnectMobileAppID = weChatEffective.MobileAppID
  706. result.WeChatConnectMobileAppSecret = weChatEffective.MobileAppSecret
  707. result.WeChatConnectMobileAppSecretConfigured = weChatEffective.MobileAppSecret != ""
  708. result.WeChatConnectOpenEnabled = weChatEffective.OpenEnabled
  709. result.WeChatConnectMPEnabled = weChatEffective.MPEnabled
  710. result.WeChatConnectMobileEnabled = weChatEffective.MobileEnabled
  711. result.WeChatConnectMode = weChatEffective.Mode
  712. result.WeChatConnectScopes = weChatEffective.Scopes
  713. result.WeChatConnectRedirectURL = weChatEffective.RedirectURL
  714. result.WeChatConnectFrontendRedirectURL = weChatEffective.FrontendRedirectURL
  715. // Model fallback settings
  716. result.EnableModelFallback = settings[SettingKeyEnableModelFallback] == "true"
  717. result.FallbackModelAnthropic = s.getStringOrDefault(settings, SettingKeyFallbackModelAnthropic, "claude-3-5-sonnet-20241022")
  718. result.FallbackModelOpenAI = s.getStringOrDefault(settings, SettingKeyFallbackModelOpenAI, "gpt-4o")
  719. result.FallbackModelGemini = s.getStringOrDefault(settings, SettingKeyFallbackModelGemini, "gemini-2.5-pro")
  720. result.FallbackModelAntigravity = s.getStringOrDefault(settings, SettingKeyFallbackModelAntigravity, "gemini-2.5-pro")
  721. // Identity patch settings (default: enabled, to preserve existing behavior)
  722. if v, ok := settings[SettingKeyEnableIdentityPatch]; ok && v != "" {
  723. result.EnableIdentityPatch = v == "true"
  724. } else {
  725. result.EnableIdentityPatch = true
  726. }
  727. result.IdentityPatchPrompt = settings[SettingKeyIdentityPatchPrompt]
  728. // Ops monitoring settings (default: enabled, fail-open)
  729. result.OpsMonitoringEnabled = !isFalseSettingValue(settings[SettingKeyOpsMonitoringEnabled])
  730. result.OpsRealtimeMonitoringEnabled = !isFalseSettingValue(settings[SettingKeyOpsRealtimeMonitoringEnabled])
  731. result.OpsQueryModeDefault = string(ParseOpsQueryMode(settings[SettingKeyOpsQueryModeDefault]))
  732. result.OpsMetricsIntervalSeconds = 60
  733. if raw := strings.TrimSpace(settings[SettingKeyOpsMetricsIntervalSeconds]); raw != "" {
  734. if v, err := strconv.Atoi(raw); err == nil {
  735. if v < 60 {
  736. v = 60
  737. }
  738. if v > 3600 {
  739. v = 3600
  740. }
  741. result.OpsMetricsIntervalSeconds = v
  742. }
  743. }
  744. // Channel monitor feature (default: enabled, 60s)
  745. result.ChannelMonitorEnabled = !isFalseSettingValue(settings[SettingKeyChannelMonitorEnabled])
  746. result.ChannelMonitorMode = normalizeChannelMonitorMode(settings[SettingKeyChannelMonitorMode])
  747. result.ChannelMonitorDefaultIntervalSeconds = parseChannelMonitorInterval(
  748. settings[SettingKeyChannelMonitorDefaultIntervalSeconds],
  749. )
  750. // 默认隐藏吞吐(迁移 206 的隐私默认):未配置时必须与 setting_public.go 的
  751. // 公开读取路径给出同一个值,否则管理端看到“未隐藏”而用户端实际已隐藏。
  752. result.ChannelMonitorHideThroughput = !isFalseSettingValue(settings[SettingKeyChannelMonitorHideThroughput])
  753. // 配额展示默认关闭且 fail-closed:仅字面 "true" 视为开启
  754. // (与 setting_public.go 公开读取路径保持一致)。
  755. result.ChannelMonitorShowQuota = settings[SettingKeyChannelMonitorShowQuota] == "true"
  756. result.ChannelMonitorHideUserRanking = isTrueSettingValue(settings[SettingKeyChannelMonitorHideUserRanking])
  757. // Grok default mapping policy
  758. result.GrokDefaultTextModel = strings.TrimSpace(settings[SettingKeyGrokDefaultTextModel])
  759. if result.GrokDefaultTextModel == "" {
  760. result.GrokDefaultTextModel = "grok-4.6"
  761. }
  762. // Default true (missing/empty → enabled) so Claude/Codex→Grok mapping keeps working.
  763. // Operators can set false to disable silent cross-client rewrite.
  764. result.GrokCrossClientModelMapEnabled = !isFalseSettingValue(settings[SettingKeyGrokCrossClientModelMapEnabled])
  765. result.GrokDefaultBaseURLMode = normalizeGrokDefaultBaseURLMode(settings[SettingKeyGrokDefaultBaseURLMode])
  766. // Available channels feature (default: disabled; strict true)
  767. result.AvailableChannelsEnabled = settings[SettingKeyAvailableChannelsEnabled] == "true"
  768. // Model plaza feature (default: disabled; strict true)
  769. result.ModelPlazaEnabled = settings[SettingKeyModelPlazaEnabled] == "true"
  770. result.ModelPlazaRequireAuth = settings[SettingKeyModelPlazaRequireAuth] == "true"
  771. result.ModelPlazaDescription = settings[SettingKeyModelPlazaDescription]
  772. result.PluginManagementEnabled = settings[SettingKeyPluginManagementEnabled] == "true"
  773. // Affiliate (邀请返利) feature (default: disabled; strict true)
  774. result.AffiliateEnabled = settings[SettingKeyAffiliateEnabled] == "true"
  775. // 风控中心功能(默认关闭,严格 true 才启用)
  776. result.RiskControlEnabled = settings[SettingKeyRiskControlEnabled] == "true"
  777. // cyber 会话屏蔽(默认关闭,TTL 默认 3600s)
  778. result.CyberSessionBlockEnabled = settings[SettingKeyCyberSessionBlockEnabled] == "true"
  779. if v, err := strconv.Atoi(strings.TrimSpace(settings[SettingKeyCyberSessionBlockTTLSeconds])); err == nil && v > 0 {
  780. result.CyberSessionBlockTTLSeconds = v
  781. } else {
  782. result.CyberSessionBlockTTLSeconds = 3600
  783. }
  784. // Claude Code version check
  785. result.MinClaudeCodeVersion = settings[SettingKeyMinClaudeCodeVersion]
  786. result.MaxClaudeCodeVersion = settings[SettingKeyMaxClaudeCodeVersion]
  787. // 分组隔离
  788. result.AllowUngroupedKeyScheduling = settings[SettingKeyAllowUngroupedKeyScheduling] == "true"
  789. // Gateway forwarding behavior (defaults: fingerprint=true, metadata_passthrough=false,
  790. // cch_signing=false, claude_oauth_system_prompt_injection=true)
  791. result.OpenAITTFTMode = normalizeOpenAITTFTMode(settings[SettingKeyOpenAITTFTMode])
  792. if v, ok := settings[SettingKeyEnableFingerprintUnification]; ok && v != "" {
  793. result.EnableFingerprintUnification = v == "true"
  794. } else {
  795. result.EnableFingerprintUnification = true // default: enabled (current behavior)
  796. }
  797. result.EnableMetadataPassthrough = settings[SettingKeyEnableMetadataPassthrough] == "true"
  798. result.EnableCCHSigning = settings[SettingKeyEnableCCHSigning] == "true"
  799. if v, ok := settings[SettingKeyEnableClaudeOAuthSystemPromptInjection]; ok && v != "" {
  800. result.EnableClaudeOAuthSystemPromptInjection = v == "true"
  801. } else {
  802. result.EnableClaudeOAuthSystemPromptInjection = true
  803. }
  804. result.ClaudeOAuthSystemPrompt = settings[SettingKeyClaudeOAuthSystemPrompt]
  805. result.ClaudeOAuthSystemPromptBlocks = settings[SettingKeyClaudeOAuthSystemPromptBlocks]
  806. result.EnableAnthropicCacheTTL1hInjection = settings[SettingKeyEnableAnthropicCacheTTL1hInjection] == "true"
  807. if v, ok := settings[SettingKeyRewriteMessageCacheControl]; ok && v != "" {
  808. result.RewriteMessageCacheControl = v == "true"
  809. } else {
  810. result.RewriteMessageCacheControl = s.defaultRewriteMessageCacheControl()
  811. }
  812. if v, ok := settings[SettingKeyEnableClientDatelineNormalization]; ok && v != "" {
  813. result.EnableClientDatelineNormalization = v == "true"
  814. } else {
  815. result.EnableClientDatelineNormalization = true
  816. }
  817. result.AntigravityUserAgentVersion = antigravity.NormalizeUserAgentVersion(settings[SettingKeyAntigravityUserAgentVersion])
  818. result.OpenAICodexUserAgent = strings.TrimSpace(settings[SettingKeyOpenAICodexUserAgent])
  819. result.OpenAICodexClientVersion = NormalizeCodexClientVersion(settings[SettingKeyOpenAICodexClientVersion])
  820. result.OpenAICodexClientVersionSynced = NormalizeCodexClientVersion(settings[SettingKeyOpenAICodexClientVersionSynced])
  821. // 自动同步默认开启:缺失/空值一律视为开启,与 enable_client_dateline_normalization 同一惯例。
  822. if v, ok := settings[SettingKeyOpenAICodexVersionAutoSyncEnabled]; ok && v != "" {
  823. result.OpenAICodexVersionAutoSyncEnabled = v == "true"
  824. } else {
  825. result.OpenAICodexVersionAutoSyncEnabled = true
  826. }
  827. // codex_cli_only 加固
  828. result.MinCodexVersion = settings[SettingKeyMinCodexVersion]
  829. result.MaxCodexVersion = settings[SettingKeyMaxCodexVersion]
  830. result.CodexCLIOnlyBlacklist = settings[SettingKeyCodexCLIOnlyBlacklist]
  831. result.CodexCLIOnlyWhitelist = settings[SettingKeyCodexCLIOnlyWhitelist]
  832. result.CodexCLIOnlyAllowAppServerClients = settings[SettingKeyCodexCLIOnlyAllowAppServerClients] == "true"
  833. if raw := strings.TrimSpace(settings[SettingKeyCodexCLIOnlyEngineFingerprintSignals]); raw != "" {
  834. result.CodexCLIOnlyEngineFingerprintSignals = raw
  835. } else {
  836. result.CodexCLIOnlyEngineFingerprintSignals = openai.DefaultEngineFingerprintSignalsJSON() // 缺失/空 → 展示默认种子
  837. }
  838. // Web search emulation: quick enabled check from the JSON config
  839. if raw := settings[SettingKeyWebSearchEmulationConfig]; raw != "" {
  840. var wsCfg WebSearchEmulationConfig
  841. if err := json.Unmarshal([]byte(raw), &wsCfg); err == nil {
  842. result.WebSearchEmulationEnabled = wsCfg.Enabled && len(wsCfg.Providers) > 0
  843. }
  844. }
  845. result.PaymentVisibleMethodAlipaySource = NormalizeVisibleMethodSource("alipay", settings[SettingPaymentVisibleMethodAlipaySource])
  846. result.PaymentVisibleMethodWxpaySource = NormalizeVisibleMethodSource("wxpay", settings[SettingPaymentVisibleMethodWxpaySource])
  847. result.PaymentVisibleMethodAlipayEnabled = settings[SettingPaymentVisibleMethodAlipayEnabled] == "true"
  848. result.PaymentVisibleMethodWxpayEnabled = settings[SettingPaymentVisibleMethodWxpayEnabled] == "true"
  849. result.OpenAILowUpstreamRatePriorityEnabled = settings[SettingKeyOpenAILowUpstreamRatePriorityEnabled] == "true"
  850. result.OpenAIOAuthSchedulingRateMultiplier = parseOpenAIOAuthSchedulingRateMultiplier(settings[SettingKeyOpenAIOAuthSchedulingRateMultiplier])
  851. result.OpenAIAdvancedSchedulerEnabled = settings[openAIAdvancedSchedulerSettingKey] == "true"
  852. result.OpenAIAdvancedSchedulerStickyWeightedEnabled = settings[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled] == "true"
  853. result.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled = settings[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled] == "true"
  854. result.OpenAIAdvancedSchedulerLBTopK = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerLBTopK])
  855. result.OpenAIAdvancedSchedulerWeightPriority = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightPriority])
  856. result.OpenAIAdvancedSchedulerWeightLoad = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightLoad])
  857. result.OpenAIAdvancedSchedulerWeightQueue = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightQueue])
  858. result.OpenAIAdvancedSchedulerWeightErrorRate = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightErrorRate])
  859. result.OpenAIAdvancedSchedulerWeightTTFT = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightTTFT])
  860. result.OpenAIAdvancedSchedulerWeightReset = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightReset])
  861. result.OpenAIAdvancedSchedulerWeightQuotaHeadroom = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom])
  862. result.OpenAIAdvancedSchedulerWeightUpstreamCost = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightUpstreamCost])
  863. result.OpenAIAdvancedSchedulerWeightPreviousResponse = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse])
  864. result.OpenAIAdvancedSchedulerWeightSessionSticky = strings.TrimSpace(settings[SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky])
  865. result.OpenAIAdvancedSchedulerEffectiveLBTopK = s.openAIAdvancedSchedulerEffectiveLBTopK()
  866. effectiveWeights := s.openAIAdvancedSchedulerEffectiveWeights()
  867. result.OpenAIAdvancedSchedulerEffectiveWeightPriority = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.Priority)
  868. result.OpenAIAdvancedSchedulerEffectiveWeightLoad = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.Load)
  869. result.OpenAIAdvancedSchedulerEffectiveWeightQueue = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.Queue)
  870. result.OpenAIAdvancedSchedulerEffectiveWeightErrorRate = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.ErrorRate)
  871. result.OpenAIAdvancedSchedulerEffectiveWeightTTFT = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.TTFT)
  872. result.OpenAIAdvancedSchedulerEffectiveWeightReset = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.Reset)
  873. result.OpenAIAdvancedSchedulerEffectiveWeightQuotaHeadroom = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.QuotaHeadroom)
  874. result.OpenAIAdvancedSchedulerEffectiveWeightUpstreamCost = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.UpstreamCost)
  875. result.OpenAIAdvancedSchedulerEffectiveWeightPreviousResponse = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.PreviousResponse)
  876. result.OpenAIAdvancedSchedulerEffectiveWeightSessionSticky = formatOpenAIAdvancedSchedulerFloat(effectiveWeights.SessionSticky)
  877. // 余额、订阅到期与账号限额通知
  878. result.BalanceLowNotifyEnabled = settings[SettingKeyBalanceLowNotifyEnabled] == "true"
  879. if v, err := strconv.ParseFloat(settings[SettingKeyBalanceLowNotifyThreshold], 64); err == nil && v >= 0 {
  880. result.BalanceLowNotifyThreshold = v
  881. }
  882. result.BalanceLowNotifyRechargeURL = settings[SettingKeyBalanceLowNotifyRechargeURL]
  883. result.SubscriptionExpiryNotifyEnabled = !isFalseSettingValue(settings[SettingKeySubscriptionExpiryNotifyEnabled])
  884. // 账号限额通知
  885. result.AccountQuotaNotifyEnabled = settings[SettingKeyAccountQuotaNotifyEnabled] == "true"
  886. if raw := strings.TrimSpace(settings[SettingKeyAccountQuotaNotifyEmails]); raw != "" {
  887. result.AccountQuotaNotifyEmails = ParseNotifyEmails(raw)
  888. }
  889. if result.AccountQuotaNotifyEmails == nil {
  890. result.AccountQuotaNotifyEmails = []NotifyEmailEntry{}
  891. }
  892. // 系统层默认 platform quota(修复 Bug B:parseSettings 不填充导致回显恒为 nil)
  893. if raw := settings[SettingKeyDefaultPlatformQuotas]; raw != "" {
  894. parsed := map[string]*DefaultPlatformQuotaSetting{}
  895. if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
  896. slog.Warn("[Setting] parseSettings: unmarshal default_platform_quotas failed", "error", err)
  897. } else {
  898. result.DefaultPlatformQuotas = parsed
  899. }
  900. }
  901. result.AccountSchedulingThresholds = defaultAccountSchedulingThresholds()
  902. if raw := strings.TrimSpace(settings[SettingKeyAccountSchedulingThresholds]); raw != "" {
  903. if thresholds, err := parseAccountSchedulingThresholdsSetting(raw); err != nil {
  904. slog.Warn("[Setting] parseSettings: unmarshal account_scheduling_thresholds failed", "error", err)
  905. } else {
  906. result.AccountSchedulingThresholds = thresholds
  907. }
  908. }
  909. result.AllowUserViewErrorRequests = settings[SettingKeyAllowUserViewErrorRequests] == "true" // default false
  910. // Publish Grok default model_mapping options for accounts with empty mapping.
  911. xai.SetRuntimeModelMappingOptions(xai.ModelMappingOptions{
  912. DefaultText: result.GrokDefaultTextModel,
  913. EnableCrossClientMap: result.GrokCrossClientModelMapEnabled,
  914. })
  915. return result
  916. }
  917. func normalizeOpenAITTFTMode(mode string) string {
  918. if strings.EqualFold(strings.TrimSpace(mode), OpenAITTFTModeVisible) {
  919. return OpenAITTFTModeVisible
  920. }
  921. return OpenAITTFTModeSemantic
  922. }
  923. func clampAffiliateRebateRate(value float64) float64 {
  924. if math.IsNaN(value) || math.IsInf(value, 0) {
  925. return AffiliateRebateRateDefault
  926. }
  927. if value < AffiliateRebateRateMin {
  928. return AffiliateRebateRateMin
  929. }
  930. if value > AffiliateRebateRateMax {
  931. return AffiliateRebateRateMax
  932. }
  933. return value
  934. }
  935. func isFalseSettingValue(value string) bool {
  936. switch strings.ToLower(strings.TrimSpace(value)) {
  937. case "false", "0", "off", "disabled":
  938. return true
  939. default:
  940. return false
  941. }
  942. }
  943. func isTrueSettingValue(value string) bool {
  944. switch strings.ToLower(strings.TrimSpace(value)) {
  945. case "true", "1", "on", "enabled":
  946. return true
  947. default:
  948. return false
  949. }
  950. }
  951. func normalizeVisibleMethodSettingSource(method, source string, enabled bool) (string, error) {
  952. _ = enabled
  953. source = strings.TrimSpace(source)
  954. if source == "" {
  955. return "", nil
  956. }
  957. normalized := NormalizeVisibleMethodSource(method, source)
  958. if normalized == "" {
  959. return "", infraerrors.BadRequest(
  960. "INVALID_PAYMENT_VISIBLE_METHOD_SOURCE",
  961. fmt.Sprintf("%s source must be one of the supported payment providers", method),
  962. )
  963. }
  964. return normalized, nil
  965. }
  966. func (s *SettingService) openAIAdvancedSchedulerEffectiveLBTopK() string {
  967. if s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.LBTopK > 0 {
  968. return strconv.Itoa(s.cfg.Gateway.OpenAIWS.LBTopK)
  969. }
  970. return "7"
  971. }
  972. func (s *SettingService) openAIAdvancedSchedulerEffectiveWeights() config.GatewayOpenAIWSSchedulerScoreWeights {
  973. defaults := config.GatewayOpenAIWSSchedulerScoreWeights{
  974. Priority: 1.0,
  975. Load: 1.0,
  976. Queue: 0.7,
  977. ErrorRate: 0.8,
  978. TTFT: 0.5,
  979. Reset: 0.0,
  980. QuotaHeadroom: 0.0,
  981. UpstreamCost: 0.0,
  982. PreviousResponse: 5.0,
  983. SessionSticky: 3.0,
  984. }
  985. if s == nil || s.cfg == nil {
  986. return defaults
  987. }
  988. weights := s.cfg.Gateway.OpenAIWS.SchedulerScoreWeights
  989. if !weights.IsValid() {
  990. return defaults
  991. }
  992. return weights
  993. }
  994. func formatOpenAIAdvancedSchedulerFloat(value float64) string {
  995. return strconv.FormatFloat(value, 'f', -1, 64)
  996. }
  997. func (s *SettingService) normalizeOpenAIAdvancedSchedulerOverrides(settings *SystemSettings) error {
  998. if rate := settings.OpenAIOAuthSchedulingRateMultiplier; rate < 0 || math.IsNaN(rate) || math.IsInf(rate, 0) {
  999. return infraerrors.BadRequest("INVALID_OPENAI_OAUTH_SCHEDULING_RATE_MULTIPLIER", "OpenAI OAuth scheduling rate multiplier must be a finite non-negative number")
  1000. }
  1001. lbTopK, err := normalizeOptionalPositiveIntString(settings.OpenAIAdvancedSchedulerLBTopK)
  1002. if err != nil {
  1003. return infraerrors.BadRequest("INVALID_OPENAI_ADVANCED_SCHEDULER_LB_TOP_K", "openai advanced scheduler TopK must be a positive integer or empty")
  1004. }
  1005. settings.OpenAIAdvancedSchedulerLBTopK = lbTopK
  1006. weights := []*string{
  1007. &settings.OpenAIAdvancedSchedulerWeightPriority,
  1008. &settings.OpenAIAdvancedSchedulerWeightLoad,
  1009. &settings.OpenAIAdvancedSchedulerWeightQueue,
  1010. &settings.OpenAIAdvancedSchedulerWeightErrorRate,
  1011. &settings.OpenAIAdvancedSchedulerWeightTTFT,
  1012. &settings.OpenAIAdvancedSchedulerWeightReset,
  1013. &settings.OpenAIAdvancedSchedulerWeightQuotaHeadroom,
  1014. &settings.OpenAIAdvancedSchedulerWeightUpstreamCost,
  1015. &settings.OpenAIAdvancedSchedulerWeightPreviousResponse,
  1016. &settings.OpenAIAdvancedSchedulerWeightSessionSticky,
  1017. }
  1018. for _, target := range weights {
  1019. normalized, err := normalizeOptionalNonNegativeFloatString(*target)
  1020. if err != nil {
  1021. return infraerrors.BadRequest("INVALID_OPENAI_ADVANCED_SCHEDULER_WEIGHT", "openai advanced scheduler weights must be non-negative numbers or empty")
  1022. }
  1023. *target = normalized
  1024. }
  1025. // 与 config.Validate 的 "scheduler_score_weights must not all be zero" 保持一致:
  1026. // 覆盖值(空则回退到生效的配置值)叠加后的基础权重和不允许为 0,
  1027. // 否则调度会静默退化为 TopK 内均匀随机。
  1028. effective := s.openAIAdvancedSchedulerEffectiveWeights()
  1029. resolved := config.GatewayOpenAIWSSchedulerScoreWeights{
  1030. Priority: resolveOpenAIAdvancedSchedulerWeight(settings.OpenAIAdvancedSchedulerWeightPriority, effective.Priority),
  1031. Load: resolveOpenAIAdvancedSchedulerWeight(settings.OpenAIAdvancedSchedulerWeightLoad, effective.Load),
  1032. Queue: resolveOpenAIAdvancedSchedulerWeight(settings.OpenAIAdvancedSchedulerWeightQueue, effective.Queue),
  1033. ErrorRate: resolveOpenAIAdvancedSchedulerWeight(settings.OpenAIAdvancedSchedulerWeightErrorRate, effective.ErrorRate),
  1034. TTFT: resolveOpenAIAdvancedSchedulerWeight(settings.OpenAIAdvancedSchedulerWeightTTFT, effective.TTFT),
  1035. Reset: resolveOpenAIAdvancedSchedulerWeight(settings.OpenAIAdvancedSchedulerWeightReset, effective.Reset),
  1036. QuotaHeadroom: resolveOpenAIAdvancedSchedulerWeight(settings.OpenAIAdvancedSchedulerWeightQuotaHeadroom, effective.QuotaHeadroom),
  1037. UpstreamCost: resolveOpenAIAdvancedSchedulerWeight(settings.OpenAIAdvancedSchedulerWeightUpstreamCost, effective.UpstreamCost),
  1038. PreviousResponse: resolveOpenAIAdvancedSchedulerWeight(settings.OpenAIAdvancedSchedulerWeightPreviousResponse, effective.PreviousResponse),
  1039. SessionSticky: resolveOpenAIAdvancedSchedulerWeight(settings.OpenAIAdvancedSchedulerWeightSessionSticky, effective.SessionSticky),
  1040. }
  1041. if !resolved.IsValid() {
  1042. return infraerrors.BadRequest("INVALID_OPENAI_ADVANCED_SCHEDULER_WEIGHT", "openai advanced scheduler weights must have finite non-zero base and total sums")
  1043. }
  1044. return nil
  1045. }
  1046. func parseOpenAIOAuthSchedulingRateMultiplier(raw string) float64 {
  1047. value, err := strconv.ParseFloat(strings.TrimSpace(raw), 64)
  1048. if err != nil || value < 0 || math.IsNaN(value) || math.IsInf(value, 0) {
  1049. return defaultOpenAIOAuthSchedulingRateMultiplier
  1050. }
  1051. return value
  1052. }
  1053. // resolveOpenAIAdvancedSchedulerWeight 返回覆盖值(已归一化的非空字符串),空则回退默认值。
  1054. func resolveOpenAIAdvancedSchedulerWeight(normalized string, fallback float64) float64 {
  1055. if normalized == "" {
  1056. return fallback
  1057. }
  1058. value, err := strconv.ParseFloat(normalized, 64)
  1059. if err != nil {
  1060. return fallback
  1061. }
  1062. return value
  1063. }
  1064. func normalizeOptionalPositiveIntString(raw string) (string, error) {
  1065. raw = strings.TrimSpace(raw)
  1066. if raw == "" {
  1067. return "", nil
  1068. }
  1069. value, err := strconv.Atoi(raw)
  1070. if err != nil || value <= 0 {
  1071. return "", fmt.Errorf("invalid positive integer")
  1072. }
  1073. return strconv.Itoa(value), nil
  1074. }
  1075. func normalizeOptionalNonNegativeFloatString(raw string) (string, error) {
  1076. raw = strings.TrimSpace(raw)
  1077. if raw == "" {
  1078. return "", nil
  1079. }
  1080. value, err := strconv.ParseFloat(raw, 64)
  1081. if err != nil || value < 0 || math.IsNaN(value) || math.IsInf(value, 0) {
  1082. return "", fmt.Errorf("invalid non-negative float")
  1083. }
  1084. return strconv.FormatFloat(value, 'f', -1, 64), nil
  1085. }
  1086. func parseDefaultSubscriptions(raw string) []DefaultSubscriptionSetting {
  1087. raw = strings.TrimSpace(raw)
  1088. if raw == "" {
  1089. return nil
  1090. }
  1091. var items []DefaultSubscriptionSetting
  1092. if err := json.Unmarshal([]byte(raw), &items); err != nil {
  1093. return nil
  1094. }
  1095. normalized := make([]DefaultSubscriptionSetting, 0, len(items))
  1096. for _, item := range items {
  1097. if item.GroupID <= 0 || item.ValidityDays <= 0 {
  1098. continue
  1099. }
  1100. if item.ValidityDays > MaxValidityDays {
  1101. item.ValidityDays = MaxValidityDays
  1102. }
  1103. normalized = append(normalized, item)
  1104. }
  1105. return normalized
  1106. }
  1107. func parseProviderDefaultGrantSettings(settings map[string]string, keys authSourceDefaultKeySet) ProviderDefaultGrantSettings {
  1108. result := ProviderDefaultGrantSettings{
  1109. Balance: defaultAuthSourceBalance,
  1110. Concurrency: defaultAuthSourceConcurrency,
  1111. Subscriptions: []DefaultSubscriptionSetting{},
  1112. GrantOnSignup: false,
  1113. GrantOnFirstBind: false,
  1114. }
  1115. if v, err := strconv.ParseFloat(strings.TrimSpace(settings[keys.balance]), 64); err == nil {
  1116. result.Balance = v
  1117. }
  1118. if v, err := strconv.Atoi(strings.TrimSpace(settings[keys.concurrency])); err == nil {
  1119. result.Concurrency = v
  1120. }
  1121. if items := parseDefaultSubscriptions(settings[keys.subscriptions]); items != nil {
  1122. result.Subscriptions = items
  1123. }
  1124. if raw, ok := settings[keys.grantOnSignup]; ok {
  1125. result.GrantOnSignup = raw == "true"
  1126. }
  1127. if raw, ok := settings[keys.grantOnFirstBind]; ok {
  1128. result.GrantOnFirstBind = raw == "true"
  1129. }
  1130. if raw := settings[keys.platformQuotas]; raw != "" {
  1131. parsed := map[string]*DefaultPlatformQuotaSetting{}
  1132. if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
  1133. slog.Warn("[Setting] parseProviderDefaultGrantSettings: unmarshal auth source platform quotas failed", "source", keys.source, "error", err)
  1134. } else {
  1135. result.PlatformQuotas = parsed
  1136. }
  1137. }
  1138. return result
  1139. }
  1140. func writeProviderDefaultGrantUpdates(updates map[string]string, keys authSourceDefaultKeySet, settings ProviderDefaultGrantSettings) {
  1141. updates[keys.balance] = strconv.FormatFloat(settings.Balance, 'f', 8, 64)
  1142. updates[keys.concurrency] = strconv.Itoa(settings.Concurrency)
  1143. subscriptions := settings.Subscriptions
  1144. if subscriptions == nil {
  1145. subscriptions = []DefaultSubscriptionSetting{}
  1146. }
  1147. raw, err := json.Marshal(subscriptions)
  1148. if err != nil {
  1149. raw = []byte("[]")
  1150. }
  1151. updates[keys.subscriptions] = string(raw)
  1152. updates[keys.grantOnSignup] = strconv.FormatBool(settings.GrantOnSignup)
  1153. updates[keys.grantOnFirstBind] = strconv.FormatBool(settings.GrantOnFirstBind)
  1154. // auth source platform quota:整体替换语义。
  1155. // nil = 请求未携带该字段,跳过写入以保留既有配置(与系统层 buildSystemSettingsUpdates 的
  1156. // DefaultPlatformQuotas nil 守卫一致);非 nil(含空 map)才整体替换。二者语义不可混同。
  1157. if keys.platformQuotas != "" && settings.PlatformQuotas != nil {
  1158. blob, err := json.Marshal(settings.PlatformQuotas)
  1159. if err != nil {
  1160. blob = []byte("{}")
  1161. }
  1162. updates[keys.platformQuotas] = string(blob)
  1163. }
  1164. }
  1165. func mergeProviderDefaultGrantSettings(globalDefaults ProviderDefaultGrantSettings, providerDefaults ProviderDefaultGrantSettings) ProviderDefaultGrantSettings {
  1166. result := ProviderDefaultGrantSettings{
  1167. Balance: globalDefaults.Balance,
  1168. Concurrency: globalDefaults.Concurrency,
  1169. Subscriptions: append([]DefaultSubscriptionSetting(nil), globalDefaults.Subscriptions...),
  1170. GrantOnSignup: providerDefaults.GrantOnSignup,
  1171. GrantOnFirstBind: providerDefaults.GrantOnFirstBind,
  1172. }
  1173. // 注意:不能把 parse 默认值 (defaultAuthSourceBalance / defaultAuthSourceConcurrency)
  1174. // 当作"未配置"哨兵——admin 完全有权显式设成相同的值,那时仍应覆盖 globalDefaults。
  1175. // 旧实现的 `!= defaultAuthSourceConcurrency` 会把 admin 设的 5 与 fallback 5 混淆,
  1176. // 导致渠道发放退回到全局默认(如 1),表现为"管理员设 5、新用户实际拿 1"。
  1177. if providerDefaults.Balance >= 0 {
  1178. result.Balance = providerDefaults.Balance
  1179. }
  1180. if providerDefaults.Concurrency > 0 {
  1181. result.Concurrency = providerDefaults.Concurrency
  1182. }
  1183. if len(providerDefaults.Subscriptions) > 0 {
  1184. result.Subscriptions = append([]DefaultSubscriptionSetting(nil), providerDefaults.Subscriptions...)
  1185. }
  1186. return result
  1187. }
  1188. func parseTablePreferences(defaultPageSizeRaw, optionsRaw string) (int, []int) {
  1189. defaultPageSize := 20
  1190. if v, err := strconv.Atoi(strings.TrimSpace(defaultPageSizeRaw)); err == nil {
  1191. defaultPageSize = v
  1192. }
  1193. var options []int
  1194. if strings.TrimSpace(optionsRaw) != "" {
  1195. _ = json.Unmarshal([]byte(optionsRaw), &options)
  1196. }
  1197. return normalizeTablePreferences(defaultPageSize, options)
  1198. }
  1199. func normalizeTablePreferences(defaultPageSize int, options []int) (int, []int) {
  1200. const minPageSize = 5
  1201. const maxPageSize = 1000
  1202. const fallbackPageSize = 20
  1203. seen := make(map[int]struct{}, len(options))
  1204. normalizedOptions := make([]int, 0, len(options))
  1205. for _, option := range options {
  1206. if option < minPageSize || option > maxPageSize {
  1207. continue
  1208. }
  1209. if _, ok := seen[option]; ok {
  1210. continue
  1211. }
  1212. seen[option] = struct{}{}
  1213. normalizedOptions = append(normalizedOptions, option)
  1214. }
  1215. sort.Ints(normalizedOptions)
  1216. if defaultPageSize < minPageSize || defaultPageSize > maxPageSize {
  1217. defaultPageSize = fallbackPageSize
  1218. }
  1219. if len(normalizedOptions) == 0 {
  1220. normalizedOptions = []int{10, 20, 50}
  1221. }
  1222. return defaultPageSize, normalizedOptions
  1223. }