setting_update.go 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "log/slog"
  8. "math"
  9. "strconv"
  10. "strings"
  11. "time"
  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. )
  16. // OmittedSettingKeys marks setting keys the caller's payload never carried.
  17. // SystemSettings is a plain struct, so a field the caller omitted arrives as a
  18. // zero value and is indistinguishable from a deliberate clear. Listing the key
  19. // here drops it from the write, leaving the stored value in place.
  20. //
  21. // A nil or empty set keeps whole-document semantics: every key is written.
  22. type OmittedSettingKeys map[string]struct{}
  23. func (o OmittedSettingKeys) dropFrom(updates map[string]string) {
  24. for key := range o {
  25. delete(updates, key)
  26. }
  27. }
  28. // UpdateSettings 更新系统设置
  29. func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSettings) error {
  30. return s.UpdateSettingsOmitting(ctx, settings, nil)
  31. }
  32. // UpdateSettingsOmitting persists system settings, leaving the keys in omitted
  33. // at their stored value.
  34. func (s *SettingService) UpdateSettingsOmitting(ctx context.Context, settings *SystemSettings, omitted OmittedSettingKeys) error {
  35. updates, err := s.buildSystemSettingsUpdates(ctx, settings)
  36. if err != nil {
  37. return err
  38. }
  39. omitted.dropFrom(updates)
  40. if err := s.settingRepo.SetMultiple(ctx, updates); err != nil {
  41. return err
  42. }
  43. s.refreshCachedSettingsAfterWrite(ctx, settings, omitted)
  44. return nil
  45. }
  46. // UpdateSettingsWithAuthSourceDefaults persists system settings and auth-source defaults in a single write.
  47. func (s *SettingService) UpdateSettingsWithAuthSourceDefaults(ctx context.Context, settings *SystemSettings, authDefaults *AuthSourceDefaultSettings) error {
  48. return s.UpdateSettingsWithAuthSourceDefaultsOmitting(ctx, settings, authDefaults, nil)
  49. }
  50. // UpdateSettingsWithAuthSourceDefaultsOmitting persists system settings and
  51. // auth-source defaults in a single write, leaving the keys in omitted at their
  52. // stored value.
  53. func (s *SettingService) UpdateSettingsWithAuthSourceDefaultsOmitting(ctx context.Context, settings *SystemSettings, authDefaults *AuthSourceDefaultSettings, omitted OmittedSettingKeys) error {
  54. updates, err := s.buildSystemSettingsUpdates(ctx, settings)
  55. if err != nil {
  56. return err
  57. }
  58. authSourceUpdates, err := s.buildAuthSourceDefaultUpdates(ctx, authDefaults)
  59. if err != nil {
  60. return err
  61. }
  62. for key, value := range authSourceUpdates {
  63. updates[key] = value
  64. }
  65. omitted.dropFrom(updates)
  66. if err := s.settingRepo.SetMultiple(ctx, updates); err != nil {
  67. return err
  68. }
  69. s.refreshCachedSettingsAfterWrite(ctx, settings, omitted)
  70. return nil
  71. }
  72. // refreshCachedSettingsAfterWrite keeps the in-process caches in step with the
  73. // write that just landed. A partial payload carries zero values for the fields
  74. // it omitted, so in that case the caches are rebuilt from storage rather than
  75. // from the request struct.
  76. func (s *SettingService) refreshCachedSettingsAfterWrite(ctx context.Context, settings *SystemSettings, omitted OmittedSettingKeys) {
  77. if len(omitted) == 0 {
  78. s.refreshCachedSettings(settings)
  79. return
  80. }
  81. stored, err := s.GetAllSettings(ctx)
  82. if err != nil {
  83. slog.Warn("refresh cached settings after partial update failed", "error", err)
  84. return
  85. }
  86. s.refreshCachedSettings(stored)
  87. }
  88. func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, settings *SystemSettings) (map[string]string, error) {
  89. if err := s.validateDefaultSubscriptionGroups(ctx, settings.DefaultSubscriptions); err != nil {
  90. return nil, err
  91. }
  92. normalizedWhitelist, err := NormalizeRegistrationEmailSuffixWhitelist(settings.RegistrationEmailSuffixWhitelist)
  93. if err != nil {
  94. return nil, infraerrors.BadRequest("INVALID_REGISTRATION_EMAIL_SUFFIX_WHITELIST", err.Error())
  95. }
  96. if normalizedWhitelist == nil {
  97. normalizedWhitelist = []string{}
  98. }
  99. settings.RegistrationEmailSuffixWhitelist = normalizedWhitelist
  100. normalizedForwardedClientIPHeaders, err := config.NormalizeForwardedClientIPHeaders(settings.ForwardedClientIPHeaders)
  101. if err != nil {
  102. return nil, infraerrors.BadRequest("INVALID_FORWARDED_CLIENT_IP_HEADERS", err.Error())
  103. }
  104. settings.ForwardedClientIPHeaders = normalizedForwardedClientIPHeaders
  105. alipaySource, err := normalizeVisibleMethodSettingSource("alipay", settings.PaymentVisibleMethodAlipaySource, settings.PaymentVisibleMethodAlipayEnabled)
  106. if err != nil {
  107. return nil, err
  108. }
  109. wxpaySource, err := normalizeVisibleMethodSettingSource("wxpay", settings.PaymentVisibleMethodWxpaySource, settings.PaymentVisibleMethodWxpayEnabled)
  110. if err != nil {
  111. return nil, err
  112. }
  113. if err := s.normalizeOpenAIAdvancedSchedulerOverrides(settings); err != nil {
  114. return nil, err
  115. }
  116. settings.PaymentVisibleMethodAlipaySource = alipaySource
  117. settings.PaymentVisibleMethodWxpaySource = wxpaySource
  118. settings.WeChatConnectAppID = strings.TrimSpace(settings.WeChatConnectAppID)
  119. settings.WeChatConnectAppSecret = strings.TrimSpace(settings.WeChatConnectAppSecret)
  120. settings.WeChatConnectOpenAppID = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectOpenAppID, settings.WeChatConnectAppID))
  121. settings.WeChatConnectOpenAppSecret = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectOpenAppSecret, settings.WeChatConnectAppSecret))
  122. settings.WeChatConnectMPAppID = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectMPAppID, settings.WeChatConnectAppID))
  123. settings.WeChatConnectMPAppSecret = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectMPAppSecret, settings.WeChatConnectAppSecret))
  124. settings.WeChatConnectMobileAppID = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectMobileAppID, settings.WeChatConnectAppID))
  125. settings.WeChatConnectMobileAppSecret = strings.TrimSpace(firstNonEmpty(settings.WeChatConnectMobileAppSecret, settings.WeChatConnectAppSecret))
  126. settings.WeChatConnectMode = normalizeWeChatConnectStoredMode(
  127. settings.WeChatConnectOpenEnabled,
  128. settings.WeChatConnectMPEnabled,
  129. settings.WeChatConnectMobileEnabled,
  130. settings.WeChatConnectMode,
  131. )
  132. settings.WeChatConnectScopes = normalizeWeChatConnectScopeSetting(settings.WeChatConnectScopes, settings.WeChatConnectMode)
  133. settings.WeChatConnectRedirectURL = strings.TrimSpace(settings.WeChatConnectRedirectURL)
  134. settings.WeChatConnectFrontendRedirectURL = strings.TrimSpace(settings.WeChatConnectFrontendRedirectURL)
  135. if settings.WeChatConnectFrontendRedirectURL == "" {
  136. settings.WeChatConnectFrontendRedirectURL = defaultWeChatConnectFrontend
  137. }
  138. settings.GitHubOAuthRedirectURL = strings.TrimSpace(settings.GitHubOAuthRedirectURL)
  139. settings.GitHubOAuthFrontendRedirectURL = strings.TrimSpace(settings.GitHubOAuthFrontendRedirectURL)
  140. if settings.GitHubOAuthFrontendRedirectURL == "" {
  141. settings.GitHubOAuthFrontendRedirectURL = defaultGitHubOAuthFrontend
  142. }
  143. settings.GoogleOAuthRedirectURL = strings.TrimSpace(settings.GoogleOAuthRedirectURL)
  144. settings.GoogleOAuthFrontendRedirectURL = strings.TrimSpace(settings.GoogleOAuthFrontendRedirectURL)
  145. if settings.GoogleOAuthFrontendRedirectURL == "" {
  146. settings.GoogleOAuthFrontendRedirectURL = defaultGoogleOAuthFrontend
  147. }
  148. updates := make(map[string]string)
  149. // 注册设置
  150. updates[SettingKeyRegistrationEnabled] = strconv.FormatBool(settings.RegistrationEnabled)
  151. updates[SettingKeyEmailVerifyEnabled] = strconv.FormatBool(settings.EmailVerifyEnabled)
  152. registrationEmailSuffixWhitelistJSON, err := json.Marshal(settings.RegistrationEmailSuffixWhitelist)
  153. if err != nil {
  154. return nil, fmt.Errorf("marshal registration email suffix whitelist: %w", err)
  155. }
  156. updates[SettingKeyRegistrationEmailSuffixWhitelist] = string(registrationEmailSuffixWhitelistJSON)
  157. updates[SettingKeyRegistrationEmailDomainQuotaEnabled] = strconv.FormatBool(settings.RegistrationEmailDomainQuotaEnabled)
  158. updates[SettingKeyPromoCodeEnabled] = strconv.FormatBool(settings.PromoCodeEnabled)
  159. updates[SettingKeyPasswordResetEnabled] = strconv.FormatBool(settings.PasswordResetEnabled)
  160. updates[SettingKeyFrontendURL] = settings.FrontendURL
  161. updates[SettingKeyInvitationCodeEnabled] = strconv.FormatBool(settings.InvitationCodeEnabled)
  162. updates[SettingKeyTotpEnabled] = strconv.FormatBool(settings.TotpEnabled)
  163. updates[SettingKeyPasskeyEnabled] = strconv.FormatBool(settings.PasskeyEnabled)
  164. updates[SettingKeySessionBindingEnabled] = strconv.FormatBool(settings.SessionBindingEnabled)
  165. updates[SettingKeyStepUpEnabled] = strconv.FormatBool(settings.StepUpEnabled)
  166. updates[SettingKeyAuditLogRetentionDays] = strconv.Itoa(settings.AuditLogRetentionDays)
  167. settings.LoginAgreementMode = normalizeLoginAgreementMode(settings.LoginAgreementMode)
  168. settings.LoginAgreementUpdatedAt = strings.TrimSpace(settings.LoginAgreementUpdatedAt)
  169. if settings.LoginAgreementUpdatedAt == "" {
  170. settings.LoginAgreementUpdatedAt = defaultLoginAgreementDate
  171. }
  172. loginAgreementDocumentsJSON, err := marshalLoginAgreementDocuments(settings.LoginAgreementDocuments)
  173. if err != nil {
  174. return nil, err
  175. }
  176. updates[SettingKeyLoginAgreementEnabled] = strconv.FormatBool(settings.LoginAgreementEnabled)
  177. updates[SettingKeyLoginAgreementMode] = settings.LoginAgreementMode
  178. updates[SettingKeyLoginAgreementUpdatedAt] = settings.LoginAgreementUpdatedAt
  179. updates[SettingKeyLoginAgreementDocuments] = loginAgreementDocumentsJSON
  180. // 邮件服务设置(只有非空才更新密码)
  181. updates[SettingKeySMTPHost] = settings.SMTPHost
  182. updates[SettingKeySMTPPort] = strconv.Itoa(settings.SMTPPort)
  183. updates[SettingKeySMTPUsername] = settings.SMTPUsername
  184. if settings.SMTPPassword != "" {
  185. updates[SettingKeySMTPPassword] = settings.SMTPPassword
  186. }
  187. updates[SettingKeySMTPFrom] = settings.SMTPFrom
  188. updates[SettingKeySMTPFromName] = settings.SMTPFromName
  189. updates[SettingKeySMTPUseTLS] = strconv.FormatBool(settings.SMTPUseTLS)
  190. // Cloudflare Turnstile 设置(只有非空才更新密钥)
  191. updates[SettingKeyTurnstileEnabled] = strconv.FormatBool(settings.TurnstileEnabled)
  192. updates[SettingKeyTurnstileSiteKey] = settings.TurnstileSiteKey
  193. if settings.TurnstileSecretKey != "" {
  194. updates[SettingKeyTurnstileSecretKey] = settings.TurnstileSecretKey
  195. }
  196. updates[SettingKeyTencentCaptchaEnabled] = strconv.FormatBool(settings.TencentCaptchaEnabled)
  197. updates[SettingKeyTencentCaptchaAppID] = settings.TencentCaptchaAppID
  198. if settings.TencentCaptchaAppSecretKey != "" {
  199. updates[SettingKeyTencentCaptchaAppSecretKey] = settings.TencentCaptchaAppSecretKey
  200. }
  201. if settings.TencentCaptchaCloudSecretID != "" {
  202. updates[SettingKeyTencentCaptchaCloudSecretID] = settings.TencentCaptchaCloudSecretID
  203. }
  204. if settings.TencentCaptchaCloudSecretKey != "" {
  205. updates[SettingKeyTencentCaptchaCloudSecretKey] = settings.TencentCaptchaCloudSecretKey
  206. }
  207. updates[SettingKeyTencentCaptchaRegion] = normalizeTencentCaptchaRegion(settings.TencentCaptchaRegion)
  208. // 阿里云验证码 2.0 设置(只有非空才更新密钥)
  209. updates[SettingKeyAliyunCaptchaEnabled] = strconv.FormatBool(settings.AliyunCaptchaEnabled)
  210. updates[SettingKeyAliyunCaptchaAccessKeyID] = settings.AliyunCaptchaAccessKeyID
  211. if settings.AliyunCaptchaAccessKeySecret != "" {
  212. updates[SettingKeyAliyunCaptchaAccessKeySecret] = settings.AliyunCaptchaAccessKeySecret
  213. }
  214. updates[SettingKeyAliyunCaptchaSceneID] = settings.AliyunCaptchaSceneID
  215. updates[SettingKeyAliyunCaptchaPrefix] = settings.AliyunCaptchaPrefix
  216. updates[SettingKeyAliyunCaptchaRegion] = normalizeAliyunCaptchaRegion(settings.AliyunCaptchaRegion)
  217. updates[SettingKeyAPIKeyACLTrustForwardedIP] = strconv.FormatBool(settings.APIKeyACLTrustForwardedIP)
  218. forwardedClientIPHeadersJSON, err := json.Marshal(settings.ForwardedClientIPHeaders)
  219. if err != nil {
  220. return nil, fmt.Errorf("marshal forwarded client IP headers: %w", err)
  221. }
  222. updates[SettingKeyForwardedClientIPHeaders] = string(forwardedClientIPHeadersJSON)
  223. // LinuxDo Connect OAuth 登录
  224. updates[SettingKeyLinuxDoConnectEnabled] = strconv.FormatBool(settings.LinuxDoConnectEnabled)
  225. updates[SettingKeyLinuxDoConnectClientID] = settings.LinuxDoConnectClientID
  226. updates[SettingKeyLinuxDoConnectRedirectURL] = settings.LinuxDoConnectRedirectURL
  227. if settings.LinuxDoConnectClientSecret != "" {
  228. updates[SettingKeyLinuxDoConnectClientSecret] = settings.LinuxDoConnectClientSecret
  229. }
  230. // DingTalk Connect OAuth 登录
  231. updates[SettingKeyDingTalkConnectEnabled] = strconv.FormatBool(settings.DingTalkConnectEnabled)
  232. updates[SettingKeyDingTalkConnectClientID] = settings.DingTalkConnectClientID
  233. updates[SettingKeyDingTalkConnectRedirectURL] = settings.DingTalkConnectRedirectURL
  234. if settings.DingTalkConnectClientSecret != "" {
  235. updates[SettingKeyDingTalkConnectClientSecret] = settings.DingTalkConnectClientSecret
  236. }
  237. updates[SettingKeyDingTalkConnectCorpRestrictionPolicy] = settings.DingTalkConnectCorpRestrictionPolicy
  238. updates[SettingKeyDingTalkConnectInternalCorpID] = settings.DingTalkConnectInternalCorpID
  239. updates[SettingKeyDingTalkConnectBypassRegistration] = strconv.FormatBool(settings.DingTalkConnectBypassRegistration)
  240. updates[SettingKeyDingTalkConnectSyncCorpEmail] = strconv.FormatBool(settings.DingTalkConnectSyncCorpEmail)
  241. updates[SettingKeyDingTalkConnectSyncDisplayName] = strconv.FormatBool(settings.DingTalkConnectSyncDisplayName)
  242. updates[SettingKeyDingTalkConnectSyncDept] = strconv.FormatBool(settings.DingTalkConnectSyncDept)
  243. updates[SettingKeyDingTalkConnectSyncCorpEmailAttrKey] = settings.DingTalkConnectSyncCorpEmailAttrKey
  244. updates[SettingKeyDingTalkConnectSyncDisplayNameAttrKey] = settings.DingTalkConnectSyncDisplayNameAttrKey
  245. updates[SettingKeyDingTalkConnectSyncDeptAttrKey] = settings.DingTalkConnectSyncDeptAttrKey
  246. updates[SettingKeyDingTalkConnectSyncCorpEmailAttrName] = settings.DingTalkConnectSyncCorpEmailAttrName
  247. updates[SettingKeyDingTalkConnectSyncDisplayNameAttrName] = settings.DingTalkConnectSyncDisplayNameAttrName
  248. updates[SettingKeyDingTalkConnectSyncDeptAttrName] = settings.DingTalkConnectSyncDeptAttrName
  249. // Generic OIDC OAuth 登录
  250. updates[SettingKeyOIDCConnectEnabled] = strconv.FormatBool(settings.OIDCConnectEnabled)
  251. updates[SettingKeyOIDCConnectProviderName] = settings.OIDCConnectProviderName
  252. updates[SettingKeyOIDCConnectClientID] = settings.OIDCConnectClientID
  253. updates[SettingKeyOIDCConnectIssuerURL] = settings.OIDCConnectIssuerURL
  254. updates[SettingKeyOIDCConnectDiscoveryURL] = settings.OIDCConnectDiscoveryURL
  255. updates[SettingKeyOIDCConnectAuthorizeURL] = settings.OIDCConnectAuthorizeURL
  256. updates[SettingKeyOIDCConnectTokenURL] = settings.OIDCConnectTokenURL
  257. updates[SettingKeyOIDCConnectUserInfoURL] = settings.OIDCConnectUserInfoURL
  258. updates[SettingKeyOIDCConnectJWKSURL] = settings.OIDCConnectJWKSURL
  259. updates[SettingKeyOIDCConnectScopes] = settings.OIDCConnectScopes
  260. updates[SettingKeyOIDCConnectRedirectURL] = settings.OIDCConnectRedirectURL
  261. updates[SettingKeyOIDCConnectFrontendRedirectURL] = settings.OIDCConnectFrontendRedirectURL
  262. updates[SettingKeyOIDCConnectTokenAuthMethod] = settings.OIDCConnectTokenAuthMethod
  263. updates[SettingKeyOIDCConnectUsePKCE] = strconv.FormatBool(settings.OIDCConnectUsePKCE)
  264. updates[SettingKeyOIDCConnectValidateIDToken] = strconv.FormatBool(settings.OIDCConnectValidateIDToken)
  265. updates[SettingKeyOIDCConnectAllowedSigningAlgs] = settings.OIDCConnectAllowedSigningAlgs
  266. updates[SettingKeyOIDCConnectClockSkewSeconds] = strconv.Itoa(settings.OIDCConnectClockSkewSeconds)
  267. updates[SettingKeyOIDCConnectRequireEmailVerified] = strconv.FormatBool(settings.OIDCConnectRequireEmailVerified)
  268. updates[SettingKeyOIDCConnectUserInfoEmailPath] = settings.OIDCConnectUserInfoEmailPath
  269. updates[SettingKeyOIDCConnectUserInfoIDPath] = settings.OIDCConnectUserInfoIDPath
  270. updates[SettingKeyOIDCConnectUserInfoUsernamePath] = settings.OIDCConnectUserInfoUsernamePath
  271. if settings.OIDCConnectClientSecret != "" {
  272. updates[SettingKeyOIDCConnectClientSecret] = settings.OIDCConnectClientSecret
  273. }
  274. // GitHub / Google 邮箱快捷登录
  275. updates[SettingKeyGitHubOAuthEnabled] = strconv.FormatBool(settings.GitHubOAuthEnabled)
  276. updates[SettingKeyGitHubOAuthClientID] = strings.TrimSpace(settings.GitHubOAuthClientID)
  277. updates[SettingKeyGitHubOAuthRedirectURL] = settings.GitHubOAuthRedirectURL
  278. updates[SettingKeyGitHubOAuthFrontendRedirectURL] = settings.GitHubOAuthFrontendRedirectURL
  279. if settings.GitHubOAuthClientSecret != "" {
  280. updates[SettingKeyGitHubOAuthClientSecret] = strings.TrimSpace(settings.GitHubOAuthClientSecret)
  281. }
  282. updates[SettingKeyGoogleOAuthEnabled] = strconv.FormatBool(settings.GoogleOAuthEnabled)
  283. updates[SettingKeyGoogleOAuthClientID] = strings.TrimSpace(settings.GoogleOAuthClientID)
  284. updates[SettingKeyGoogleOAuthRedirectURL] = settings.GoogleOAuthRedirectURL
  285. updates[SettingKeyGoogleOAuthFrontendRedirectURL] = settings.GoogleOAuthFrontendRedirectURL
  286. if settings.GoogleOAuthClientSecret != "" {
  287. updates[SettingKeyGoogleOAuthClientSecret] = strings.TrimSpace(settings.GoogleOAuthClientSecret)
  288. }
  289. // WeChat Connect OAuth 登录
  290. updates[SettingKeyWeChatConnectEnabled] = strconv.FormatBool(settings.WeChatConnectEnabled)
  291. updates[SettingKeyWeChatConnectAppID] = settings.WeChatConnectAppID
  292. updates[SettingKeyWeChatConnectOpenAppID] = settings.WeChatConnectOpenAppID
  293. updates[SettingKeyWeChatConnectMPAppID] = settings.WeChatConnectMPAppID
  294. updates[SettingKeyWeChatConnectMobileAppID] = settings.WeChatConnectMobileAppID
  295. updates[SettingKeyWeChatConnectOpenEnabled] = strconv.FormatBool(settings.WeChatConnectOpenEnabled)
  296. updates[SettingKeyWeChatConnectMPEnabled] = strconv.FormatBool(settings.WeChatConnectMPEnabled)
  297. updates[SettingKeyWeChatConnectMobileEnabled] = strconv.FormatBool(settings.WeChatConnectMobileEnabled)
  298. updates[SettingKeyWeChatConnectMode] = settings.WeChatConnectMode
  299. updates[SettingKeyWeChatConnectScopes] = settings.WeChatConnectScopes
  300. updates[SettingKeyWeChatConnectRedirectURL] = settings.WeChatConnectRedirectURL
  301. updates[SettingKeyWeChatConnectFrontendRedirectURL] = settings.WeChatConnectFrontendRedirectURL
  302. if settings.WeChatConnectAppSecret != "" {
  303. updates[SettingKeyWeChatConnectAppSecret] = settings.WeChatConnectAppSecret
  304. }
  305. if settings.WeChatConnectOpenAppSecret != "" {
  306. updates[SettingKeyWeChatConnectOpenAppSecret] = settings.WeChatConnectOpenAppSecret
  307. }
  308. if settings.WeChatConnectMPAppSecret != "" {
  309. updates[SettingKeyWeChatConnectMPAppSecret] = settings.WeChatConnectMPAppSecret
  310. }
  311. if settings.WeChatConnectMobileAppSecret != "" {
  312. updates[SettingKeyWeChatConnectMobileAppSecret] = settings.WeChatConnectMobileAppSecret
  313. }
  314. // OEM设置
  315. updates[SettingKeySiteName] = settings.SiteName
  316. updates[SettingKeySiteLogo] = settings.SiteLogo
  317. updates[SettingKeySiteSubtitle] = settings.SiteSubtitle
  318. updates[SettingKeyAPIBaseURL] = settings.APIBaseURL
  319. updates[SettingKeyContactInfo] = settings.ContactInfo
  320. updates[SettingKeyDocURL] = settings.DocURL
  321. updates[SettingKeyHomeContent] = settings.HomeContent
  322. updates[SettingKeyCompactHomeEnabled] = strconv.FormatBool(settings.CompactHomeEnabled)
  323. updates[SettingKeyHideCcsImportButton] = strconv.FormatBool(settings.HideCcsImportButton)
  324. updates[SettingKeyPurchaseSubscriptionEnabled] = strconv.FormatBool(settings.PurchaseSubscriptionEnabled)
  325. updates[SettingKeyPurchaseSubscriptionURL] = strings.TrimSpace(settings.PurchaseSubscriptionURL)
  326. tableDefaultPageSize, tablePageSizeOptions := normalizeTablePreferences(
  327. settings.TableDefaultPageSize,
  328. settings.TablePageSizeOptions,
  329. )
  330. updates[SettingKeyTableDefaultPageSize] = strconv.Itoa(tableDefaultPageSize)
  331. tablePageSizeOptionsJSON, err := json.Marshal(tablePageSizeOptions)
  332. if err != nil {
  333. return nil, fmt.Errorf("marshal table page size options: %w", err)
  334. }
  335. updates[SettingKeyTablePageSizeOptions] = string(tablePageSizeOptionsJSON)
  336. updates[SettingKeyCustomMenuItems] = settings.CustomMenuItems
  337. updates[SettingKeyCustomEndpoints] = settings.CustomEndpoints
  338. // 默认配置
  339. updates[SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency)
  340. updates[SettingKeyDefaultBalance] = strconv.FormatFloat(settings.DefaultBalance, 'f', 8, 64)
  341. settings.AffiliateRebateRate = clampAffiliateRebateRate(settings.AffiliateRebateRate)
  342. updates[SettingKeyAffiliateRebateRate] = strconv.FormatFloat(settings.AffiliateRebateRate, 'f', 8, 64)
  343. if settings.AffiliateRebateFreezeHours < 0 {
  344. settings.AffiliateRebateFreezeHours = AffiliateRebateFreezeHoursDefault
  345. }
  346. if settings.AffiliateRebateFreezeHours > AffiliateRebateFreezeHoursMax {
  347. settings.AffiliateRebateFreezeHours = AffiliateRebateFreezeHoursMax
  348. }
  349. updates[SettingKeyAffiliateRebateFreezeHours] = strconv.Itoa(settings.AffiliateRebateFreezeHours)
  350. if settings.AffiliateRebateDurationDays < 0 {
  351. settings.AffiliateRebateDurationDays = AffiliateRebateDurationDaysDefault
  352. }
  353. if settings.AffiliateRebateDurationDays > AffiliateRebateDurationDaysMax {
  354. settings.AffiliateRebateDurationDays = AffiliateRebateDurationDaysMax
  355. }
  356. updates[SettingKeyAffiliateRebateDurationDays] = strconv.Itoa(settings.AffiliateRebateDurationDays)
  357. if settings.AffiliateRebatePerInviteeCap < 0 {
  358. settings.AffiliateRebatePerInviteeCap = AffiliateRebatePerInviteeCapDefault
  359. }
  360. updates[SettingKeyAffiliateRebatePerInviteeCap] = strconv.FormatFloat(settings.AffiliateRebatePerInviteeCap, 'f', 8, 64)
  361. updates[SettingKeyAffiliateAdminRechargeEnabled] = strconv.FormatBool(settings.AdminRechargeRebateEnabled)
  362. updates[SettingKeyDefaultUserRPMLimit] = strconv.Itoa(settings.DefaultUserRPMLimit)
  363. defaultSubsJSON, err := json.Marshal(settings.DefaultSubscriptions)
  364. if err != nil {
  365. return nil, fmt.Errorf("marshal default subscriptions: %w", err)
  366. }
  367. updates[SettingKeyDefaultSubscriptions] = string(defaultSubsJSON)
  368. // Model fallback configuration
  369. updates[SettingKeyEnableModelFallback] = strconv.FormatBool(settings.EnableModelFallback)
  370. updates[SettingKeyFallbackModelAnthropic] = settings.FallbackModelAnthropic
  371. updates[SettingKeyFallbackModelOpenAI] = settings.FallbackModelOpenAI
  372. updates[SettingKeyFallbackModelGemini] = settings.FallbackModelGemini
  373. updates[SettingKeyFallbackModelAntigravity] = settings.FallbackModelAntigravity
  374. // Identity patch configuration (Claude -> Gemini)
  375. updates[SettingKeyEnableIdentityPatch] = strconv.FormatBool(settings.EnableIdentityPatch)
  376. updates[SettingKeyIdentityPatchPrompt] = settings.IdentityPatchPrompt
  377. // Ops monitoring (vNext)
  378. updates[SettingKeyOpsMonitoringEnabled] = strconv.FormatBool(settings.OpsMonitoringEnabled)
  379. updates[SettingKeyOpsRealtimeMonitoringEnabled] = strconv.FormatBool(settings.OpsRealtimeMonitoringEnabled)
  380. updates[SettingKeyOpsQueryModeDefault] = string(ParseOpsQueryMode(settings.OpsQueryModeDefault))
  381. if settings.OpsMetricsIntervalSeconds > 0 {
  382. updates[SettingKeyOpsMetricsIntervalSeconds] = strconv.Itoa(settings.OpsMetricsIntervalSeconds)
  383. }
  384. // Channel monitor feature switch
  385. updates[SettingKeyChannelMonitorEnabled] = strconv.FormatBool(settings.ChannelMonitorEnabled)
  386. updates[SettingKeyChannelMonitorMode] = normalizeChannelMonitorMode(settings.ChannelMonitorMode)
  387. if v := clampChannelMonitorInterval(settings.ChannelMonitorDefaultIntervalSeconds); v > 0 {
  388. updates[SettingKeyChannelMonitorDefaultIntervalSeconds] = strconv.Itoa(v)
  389. }
  390. updates[SettingKeyChannelMonitorHideThroughput] = strconv.FormatBool(settings.ChannelMonitorHideThroughput)
  391. updates[SettingKeyChannelMonitorShowQuota] = strconv.FormatBool(settings.ChannelMonitorShowQuota)
  392. updates[SettingKeyChannelMonitorHideUserRanking] = strconv.FormatBool(settings.ChannelMonitorHideUserRanking)
  393. // Grok model mapping policy
  394. if v := strings.TrimSpace(settings.GrokDefaultTextModel); v != "" {
  395. updates[SettingKeyGrokDefaultTextModel] = v
  396. } else {
  397. updates[SettingKeyGrokDefaultTextModel] = "grok-4.6"
  398. }
  399. updates[SettingKeyGrokCrossClientModelMapEnabled] = strconv.FormatBool(settings.GrokCrossClientModelMapEnabled)
  400. updates[SettingKeyGrokDefaultBaseURLMode] = normalizeGrokDefaultBaseURLMode(settings.GrokDefaultBaseURLMode)
  401. // Available channels feature switch
  402. updates[SettingKeyAvailableChannelsEnabled] = strconv.FormatBool(settings.AvailableChannelsEnabled)
  403. // Model plaza feature switches + description
  404. updates[SettingKeyModelPlazaEnabled] = strconv.FormatBool(settings.ModelPlazaEnabled)
  405. updates[SettingKeyModelPlazaRequireAuth] = strconv.FormatBool(settings.ModelPlazaRequireAuth)
  406. updates[SettingKeyModelPlazaDescription] = settings.ModelPlazaDescription
  407. updates[SettingKeyPluginManagementEnabled] = strconv.FormatBool(settings.PluginManagementEnabled)
  408. // Affiliate (邀请返利) feature switch
  409. updates[SettingKeyAffiliateEnabled] = strconv.FormatBool(settings.AffiliateEnabled)
  410. // 风控中心功能开关
  411. updates[SettingKeyRiskControlEnabled] = strconv.FormatBool(settings.RiskControlEnabled)
  412. // cyber 会话屏蔽开关 + TTL
  413. updates[SettingKeyCyberSessionBlockEnabled] = strconv.FormatBool(settings.CyberSessionBlockEnabled)
  414. if settings.CyberSessionBlockTTLSeconds > 0 {
  415. updates[SettingKeyCyberSessionBlockTTLSeconds] = strconv.Itoa(settings.CyberSessionBlockTTLSeconds)
  416. }
  417. // Claude Code version check
  418. updates[SettingKeyMinClaudeCodeVersion] = settings.MinClaudeCodeVersion
  419. updates[SettingKeyMaxClaudeCodeVersion] = settings.MaxClaudeCodeVersion
  420. // 分组隔离
  421. updates[SettingKeyAllowUngroupedKeyScheduling] = strconv.FormatBool(settings.AllowUngroupedKeyScheduling)
  422. // Backend Mode
  423. updates[SettingKeyBackendModeEnabled] = strconv.FormatBool(settings.BackendModeEnabled)
  424. // Gateway forwarding behavior
  425. mode := normalizeOpenAITTFTMode(settings.OpenAITTFTMode)
  426. if strings.TrimSpace(settings.OpenAITTFTMode) != "" && strings.ToLower(strings.TrimSpace(settings.OpenAITTFTMode)) != OpenAITTFTModeSemantic && strings.ToLower(strings.TrimSpace(settings.OpenAITTFTMode)) != OpenAITTFTModeVisible {
  427. return nil, fmt.Errorf("%s must be one of: %s/%s", SettingKeyOpenAITTFTMode, OpenAITTFTModeSemantic, OpenAITTFTModeVisible)
  428. }
  429. updates[SettingKeyOpenAITTFTMode] = mode
  430. updates[SettingKeyEnableFingerprintUnification] = strconv.FormatBool(settings.EnableFingerprintUnification)
  431. updates[SettingKeyEnableMetadataPassthrough] = strconv.FormatBool(settings.EnableMetadataPassthrough)
  432. updates[SettingKeyEnableCCHSigning] = strconv.FormatBool(settings.EnableCCHSigning)
  433. updates[SettingKeyEnableClaudeOAuthSystemPromptInjection] = strconv.FormatBool(settings.EnableClaudeOAuthSystemPromptInjection)
  434. updates[SettingKeyClaudeOAuthSystemPrompt] = settings.ClaudeOAuthSystemPrompt
  435. if err := ValidateClaudeOAuthSystemPromptBlocksConfig(settings.ClaudeOAuthSystemPromptBlocks); err != nil {
  436. return nil, err
  437. }
  438. updates[SettingKeyClaudeOAuthSystemPromptBlocks] = settings.ClaudeOAuthSystemPromptBlocks
  439. updates[SettingKeyEnableAnthropicCacheTTL1hInjection] = strconv.FormatBool(settings.EnableAnthropicCacheTTL1hInjection)
  440. updates[SettingKeyRewriteMessageCacheControl] = strconv.FormatBool(settings.RewriteMessageCacheControl)
  441. updates[SettingKeyEnableClientDatelineNormalization] = strconv.FormatBool(settings.EnableClientDatelineNormalization)
  442. updates[SettingKeyAntigravityUserAgentVersion] = antigravity.NormalizeUserAgentVersion(settings.AntigravityUserAgentVersion)
  443. updates[SettingKeyOpenAICodexUserAgent] = strings.TrimSpace(settings.OpenAICodexUserAgent)
  444. updates[SettingKeyOpenAICodexClientVersion] = NormalizeCodexClientVersion(settings.OpenAICodexClientVersion)
  445. updates[SettingKeyOpenAICodexVersionAutoSyncEnabled] = strconv.FormatBool(settings.OpenAICodexVersionAutoSyncEnabled)
  446. // SettingKeyOpenAICodexClientVersionSynced 由自动同步任务独占写入,此处不得覆盖,
  447. // 否则面板保存会把同步结果清空。
  448. // codex_cli_only 加固
  449. updates[SettingKeyMinCodexVersion] = strings.TrimSpace(settings.MinCodexVersion)
  450. updates[SettingKeyMaxCodexVersion] = strings.TrimSpace(settings.MaxCodexVersion)
  451. updates[SettingKeyCodexCLIOnlyBlacklist] = strings.TrimSpace(settings.CodexCLIOnlyBlacklist)
  452. updates[SettingKeyCodexCLIOnlyWhitelist] = strings.TrimSpace(settings.CodexCLIOnlyWhitelist)
  453. updates[SettingKeyCodexCLIOnlyAllowAppServerClients] = strconv.FormatBool(settings.CodexCLIOnlyAllowAppServerClients)
  454. updates[SettingKeyCodexCLIOnlyEngineFingerprintSignals] = strings.TrimSpace(settings.CodexCLIOnlyEngineFingerprintSignals)
  455. updates[SettingPaymentVisibleMethodAlipaySource] = settings.PaymentVisibleMethodAlipaySource
  456. updates[SettingPaymentVisibleMethodWxpaySource] = settings.PaymentVisibleMethodWxpaySource
  457. updates[SettingPaymentVisibleMethodAlipayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodAlipayEnabled)
  458. updates[SettingPaymentVisibleMethodWxpayEnabled] = strconv.FormatBool(settings.PaymentVisibleMethodWxpayEnabled)
  459. updates[SettingKeyOpenAILowUpstreamRatePriorityEnabled] = strconv.FormatBool(settings.OpenAILowUpstreamRatePriorityEnabled)
  460. updates[SettingKeyOpenAIOAuthSchedulingRateMultiplier] = strconv.FormatFloat(settings.OpenAIOAuthSchedulingRateMultiplier, 'f', -1, 64)
  461. updates[openAIAdvancedSchedulerSettingKey] = strconv.FormatBool(settings.OpenAIAdvancedSchedulerEnabled)
  462. updates[SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled] = strconv.FormatBool(settings.OpenAIAdvancedSchedulerStickyWeightedEnabled)
  463. updates[SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled] = strconv.FormatBool(settings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled)
  464. updates[SettingKeyOpenAIAdvancedSchedulerLBTopK] = settings.OpenAIAdvancedSchedulerLBTopK
  465. updates[SettingKeyOpenAIAdvancedSchedulerWeightPriority] = settings.OpenAIAdvancedSchedulerWeightPriority
  466. updates[SettingKeyOpenAIAdvancedSchedulerWeightLoad] = settings.OpenAIAdvancedSchedulerWeightLoad
  467. updates[SettingKeyOpenAIAdvancedSchedulerWeightQueue] = settings.OpenAIAdvancedSchedulerWeightQueue
  468. updates[SettingKeyOpenAIAdvancedSchedulerWeightErrorRate] = settings.OpenAIAdvancedSchedulerWeightErrorRate
  469. updates[SettingKeyOpenAIAdvancedSchedulerWeightTTFT] = settings.OpenAIAdvancedSchedulerWeightTTFT
  470. updates[SettingKeyOpenAIAdvancedSchedulerWeightReset] = settings.OpenAIAdvancedSchedulerWeightReset
  471. updates[SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom] = settings.OpenAIAdvancedSchedulerWeightQuotaHeadroom
  472. updates[SettingKeyOpenAIAdvancedSchedulerWeightUpstreamCost] = settings.OpenAIAdvancedSchedulerWeightUpstreamCost
  473. updates[SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse] = settings.OpenAIAdvancedSchedulerWeightPreviousResponse
  474. updates[SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky] = settings.OpenAIAdvancedSchedulerWeightSessionSticky
  475. // 余额、订阅到期与账号限额通知
  476. updates[SettingKeyBalanceLowNotifyEnabled] = strconv.FormatBool(settings.BalanceLowNotifyEnabled)
  477. updates[SettingKeyBalanceLowNotifyThreshold] = strconv.FormatFloat(settings.BalanceLowNotifyThreshold, 'f', 8, 64)
  478. updates[SettingKeyBalanceLowNotifyRechargeURL] = settings.BalanceLowNotifyRechargeURL
  479. updates[SettingKeySubscriptionExpiryNotifyEnabled] = strconv.FormatBool(settings.SubscriptionExpiryNotifyEnabled)
  480. updates[SettingKeyAccountQuotaNotifyEnabled] = strconv.FormatBool(settings.AccountQuotaNotifyEnabled)
  481. updates[SettingKeyAccountQuotaNotifyEmails] = MarshalNotifyEmails(settings.AccountQuotaNotifyEmails)
  482. // 系统全局 platform quota:整体替换语义(null/缺省 = 不限制)。
  483. if settings.DefaultPlatformQuotas != nil {
  484. if err := validateDefaultPlatformQuotaMap(settings.DefaultPlatformQuotas); err != nil {
  485. return nil, err
  486. }
  487. blob, err := json.Marshal(settings.DefaultPlatformQuotas)
  488. if err != nil {
  489. return nil, fmt.Errorf("marshal default platform quotas: %w", err)
  490. }
  491. updates[SettingKeyDefaultPlatformQuotas] = string(blob)
  492. }
  493. if settings.AccountSchedulingThresholds != nil {
  494. normalized, err := validateAndNormalizeAccountSchedulingThresholds(settings.AccountSchedulingThresholds)
  495. if err != nil {
  496. return nil, err
  497. }
  498. blob, err := json.Marshal(normalized)
  499. if err != nil {
  500. return nil, fmt.Errorf("marshal account scheduling thresholds: %w", err)
  501. }
  502. updates[SettingKeyAccountSchedulingThresholds] = string(blob)
  503. }
  504. updates[SettingKeyAllowUserViewErrorRequests] = strconv.FormatBool(settings.AllowUserViewErrorRequests)
  505. return updates, nil
  506. }
  507. func defaultAccountSchedulingThresholds() map[string]int {
  508. return map[string]int{
  509. PlatformOpenAI: 100,
  510. PlatformAnthropic: 100,
  511. PlatformGrok: 100,
  512. }
  513. }
  514. func validateAndNormalizeAccountSchedulingThresholds(input map[string]int) (map[string]int, error) {
  515. normalized := defaultAccountSchedulingThresholds()
  516. for platform, value := range input {
  517. allowed := false
  518. for _, item := range AllowedSchedulingThresholdPlatforms {
  519. if item == platform {
  520. allowed = true
  521. break
  522. }
  523. }
  524. if !allowed {
  525. return nil, infraerrors.BadRequest("INVALID_ACCOUNT_SCHEDULING_THRESHOLDS", fmt.Sprintf("unknown platform %q", platform))
  526. }
  527. if value < 1 || value > 100 {
  528. return nil, infraerrors.BadRequest("INVALID_ACCOUNT_SCHEDULING_THRESHOLDS", "platform scheduling threshold must be between 1 and 100")
  529. }
  530. normalized[platform] = value
  531. }
  532. return normalized, nil
  533. }
  534. func parseAccountSchedulingThresholdsSetting(raw string) (map[string]int, error) {
  535. thresholds := defaultAccountSchedulingThresholds()
  536. raw = strings.TrimSpace(raw)
  537. if raw == "" {
  538. return thresholds, nil
  539. }
  540. parsed := map[string]int{}
  541. if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
  542. return thresholds, err
  543. }
  544. for _, platform := range AllowedSchedulingThresholdPlatforms {
  545. if value, ok := parsed[platform]; ok {
  546. thresholds[platform] = boundedIntOrDefault(value, 1, 100, 100)
  547. }
  548. }
  549. return thresholds, nil
  550. }
  551. func boundedIntOrDefault(value, minValue, maxValue, defaultValue int) int {
  552. if value < minValue || value > maxValue {
  553. return defaultValue
  554. }
  555. return value
  556. }
  557. func cloneAccountSchedulingThresholds(input map[string]int) map[string]int {
  558. if len(input) == 0 {
  559. return defaultAccountSchedulingThresholds()
  560. }
  561. cloned := make(map[string]int, len(input))
  562. for key, value := range input {
  563. cloned[key] = value
  564. }
  565. return cloned
  566. }
  567. // validateDefaultPlatformQuotaMap 校验 platform quota map 的合法性:
  568. // 平台名须在 AllowedQuotaPlatforms 白名单内,每个非 nil 上限须 finite 且 >= 0。
  569. // 系统层和 auth-source 层共用此 helper。
  570. func validateDefaultPlatformQuotaMap(m map[string]*DefaultPlatformQuotaSetting) error {
  571. for platform, pq := range m {
  572. if !IsAllowedQuotaPlatform(platform) {
  573. return infraerrors.BadRequest("INVALID_DEFAULT_PLATFORM_QUOTA", fmt.Sprintf("unknown platform %q", platform))
  574. }
  575. if pq == nil {
  576. continue
  577. }
  578. for _, v := range []*float64{pq.DailyLimitUSD, pq.WeeklyLimitUSD, pq.MonthlyLimitUSD} {
  579. if v != nil && (*v < 0 || math.IsNaN(*v) || math.IsInf(*v, 0)) {
  580. return infraerrors.BadRequest("INVALID_DEFAULT_PLATFORM_QUOTA", "platform quota limit must be a finite non-negative number")
  581. }
  582. }
  583. }
  584. return nil
  585. }
  586. func (s *SettingService) buildAuthSourceDefaultUpdates(ctx context.Context, settings *AuthSourceDefaultSettings) (map[string]string, error) {
  587. if settings == nil {
  588. return nil, nil
  589. }
  590. for _, subscriptions := range [][]DefaultSubscriptionSetting{
  591. settings.Email.Subscriptions,
  592. settings.LinuxDo.Subscriptions,
  593. settings.OIDC.Subscriptions,
  594. settings.WeChat.Subscriptions,
  595. settings.GitHub.Subscriptions,
  596. settings.Google.Subscriptions,
  597. settings.DingTalk.Subscriptions,
  598. } {
  599. if err := s.validateDefaultSubscriptionGroups(ctx, subscriptions); err != nil {
  600. return nil, err
  601. }
  602. }
  603. // 校验各 auth source 的 platform quota map(改动 C:对等系统层校验)
  604. for _, pgs := range []struct {
  605. name string
  606. pq map[string]*DefaultPlatformQuotaSetting
  607. }{
  608. {"email", settings.Email.PlatformQuotas},
  609. {"linuxdo", settings.LinuxDo.PlatformQuotas},
  610. {"oidc", settings.OIDC.PlatformQuotas},
  611. {"wechat", settings.WeChat.PlatformQuotas},
  612. {"github", settings.GitHub.PlatformQuotas},
  613. {"google", settings.Google.PlatformQuotas},
  614. {"dingtalk", settings.DingTalk.PlatformQuotas},
  615. } {
  616. if pgs.pq != nil {
  617. if err := validateDefaultPlatformQuotaMap(pgs.pq); err != nil {
  618. return nil, err
  619. }
  620. }
  621. }
  622. updates := make(map[string]string, 36)
  623. writeProviderDefaultGrantUpdates(updates, emailAuthSourceDefaultKeys, settings.Email)
  624. writeProviderDefaultGrantUpdates(updates, linuxDoAuthSourceDefaultKeys, settings.LinuxDo)
  625. writeProviderDefaultGrantUpdates(updates, oidcAuthSourceDefaultKeys, settings.OIDC)
  626. writeProviderDefaultGrantUpdates(updates, weChatAuthSourceDefaultKeys, settings.WeChat)
  627. writeProviderDefaultGrantUpdates(updates, gitHubAuthSourceDefaultKeys, settings.GitHub)
  628. writeProviderDefaultGrantUpdates(updates, googleAuthSourceDefaultKeys, settings.Google)
  629. writeProviderDefaultGrantUpdates(updates, dingTalkAuthSourceDefaultKeys, settings.DingTalk)
  630. updates[SettingKeyForceEmailOnThirdPartySignup] = strconv.FormatBool(settings.ForceEmailOnThirdPartySignup)
  631. return updates, nil
  632. }
  633. func (s *SettingService) refreshCachedSettings(settings *SystemSettings) {
  634. if settings == nil {
  635. return
  636. }
  637. // 先使 inflight singleflight 失效,再刷新缓存,缩小旧值覆盖新值的竞态窗口
  638. versionBoundsSF.Forget("version_bounds")
  639. versionBoundsCache.Store(&cachedVersionBounds{
  640. min: settings.MinClaudeCodeVersion,
  641. max: settings.MaxClaudeCodeVersion,
  642. expiresAt: time.Now().Add(versionBoundsCacheTTL).UnixNano(),
  643. })
  644. backendModeSF.Forget("backend_mode")
  645. backendModeCache.Store(&cachedBackendMode{
  646. value: settings.BackendModeEnabled,
  647. expiresAt: time.Now().Add(backendModeCacheTTL).UnixNano(),
  648. })
  649. gatewayForwardingSF.Forget("gateway_forwarding")
  650. gatewayForwardingCache.Store(&cachedGatewayForwardingSettings{
  651. openAITTFTMode: normalizeOpenAITTFTMode(settings.OpenAITTFTMode),
  652. fingerprintUnification: settings.EnableFingerprintUnification,
  653. metadataPassthrough: settings.EnableMetadataPassthrough,
  654. cchSigning: settings.EnableCCHSigning,
  655. claudeOAuthSystemPromptInjection: settings.EnableClaudeOAuthSystemPromptInjection,
  656. claudeOAuthSystemPrompt: settings.ClaudeOAuthSystemPrompt,
  657. claudeOAuthSystemPromptBlocks: settings.ClaudeOAuthSystemPromptBlocks,
  658. anthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
  659. rewriteMessageCacheControl: settings.RewriteMessageCacheControl,
  660. clientDatelineNormalization: settings.EnableClientDatelineNormalization,
  661. expiresAt: time.Now().Add(gatewayForwardingCacheTTL).UnixNano(),
  662. })
  663. s.antigravityUAVersionSF.Forget("antigravity_user_agent_version")
  664. antigravityUserAgentVersion := antigravity.NormalizeUserAgentVersion(settings.AntigravityUserAgentVersion)
  665. if antigravityUserAgentVersion == "" {
  666. antigravityUserAgentVersion = antigravity.GetDefaultUserAgentVersion()
  667. }
  668. s.antigravityUAVersionCache.Store(&cachedAntigravityUserAgentVersion{
  669. version: antigravityUserAgentVersion,
  670. expiresAt: time.Now().Add(antigravityUserAgentVersionCacheTTL).UnixNano(),
  671. })
  672. s.openAICodexUASF.Forget("openai_codex_user_agent")
  673. codexUA := strings.TrimSpace(settings.OpenAICodexUserAgent)
  674. if codexUA == "" {
  675. codexUA = DefaultOpenAICodexUserAgent
  676. }
  677. s.openAICodexUACache.Store(&cachedOpenAICodexUserAgent{
  678. value: codexUA,
  679. expiresAt: time.Now().Add(openAICodexUserAgentCacheTTL).UnixNano(),
  680. })
  681. // 版本号缓存只做失效,不在此重算:生效值还取决于自动同步写入的 synced 键,
  682. // 这里没有它的最新值,重算会把同步结果覆盖成陈旧值。
  683. s.InvalidateOpenAICodexClientVersionCache()
  684. openAIAdvancedSchedulerSettingSF.Forget(openAIAdvancedSchedulerSettingKey)
  685. openAIAdvancedSchedulerSettingCache.Store(&cachedOpenAIAdvancedSchedulerSetting{
  686. lowUpstreamRatePriorityEnabled: settings.OpenAILowUpstreamRatePriorityEnabled,
  687. oauthSchedulingRateMultiplier: settings.OpenAIOAuthSchedulingRateMultiplier,
  688. enabled: settings.OpenAIAdvancedSchedulerEnabled,
  689. stickyWeightedEnabled: settings.OpenAIAdvancedSchedulerStickyWeightedEnabled,
  690. subscriptionPriorityEnabled: settings.OpenAIAdvancedSchedulerSubscriptionPriorityEnabled,
  691. lbTopKOverride: parsePositiveIntOverride(settings.OpenAIAdvancedSchedulerLBTopK),
  692. weightOverrides: parseOpenAIAdvancedSchedulerWeightOverrides(map[string]string{
  693. SettingKeyOpenAIAdvancedSchedulerWeightPriority: settings.OpenAIAdvancedSchedulerWeightPriority,
  694. SettingKeyOpenAIAdvancedSchedulerWeightLoad: settings.OpenAIAdvancedSchedulerWeightLoad,
  695. SettingKeyOpenAIAdvancedSchedulerWeightQueue: settings.OpenAIAdvancedSchedulerWeightQueue,
  696. SettingKeyOpenAIAdvancedSchedulerWeightErrorRate: settings.OpenAIAdvancedSchedulerWeightErrorRate,
  697. SettingKeyOpenAIAdvancedSchedulerWeightTTFT: settings.OpenAIAdvancedSchedulerWeightTTFT,
  698. SettingKeyOpenAIAdvancedSchedulerWeightReset: settings.OpenAIAdvancedSchedulerWeightReset,
  699. SettingKeyOpenAIAdvancedSchedulerWeightQuotaHeadroom: settings.OpenAIAdvancedSchedulerWeightQuotaHeadroom,
  700. SettingKeyOpenAIAdvancedSchedulerWeightUpstreamCost: settings.OpenAIAdvancedSchedulerWeightUpstreamCost,
  701. SettingKeyOpenAIAdvancedSchedulerWeightPreviousResponse: settings.OpenAIAdvancedSchedulerWeightPreviousResponse,
  702. SettingKeyOpenAIAdvancedSchedulerWeightSessionSticky: settings.OpenAIAdvancedSchedulerWeightSessionSticky,
  703. }),
  704. expiresAt: time.Now().Add(openAIAdvancedSchedulerSettingCacheTTL).UnixNano(),
  705. })
  706. // Invalidate the quota auto-pause cache and let the next read trigger a fresh load.
  707. // We can't know from here whether ops_advanced_settings was also touched, so be
  708. // defensive: store an expired entry — GetOpenAIQuotaAutoPauseSettings will serve
  709. // stale and kick off an async refresh, never blocking the request that follows.
  710. s.openAIQuotaAutoPauseSettingsSF.Forget(openAIQuotaAutoPauseSettingsRefreshKey)
  711. if cached, _ := s.openAIQuotaAutoPauseSettingsCache.Load().(*cachedOpenAIQuotaAutoPauseSettings); cached != nil {
  712. s.openAIQuotaAutoPauseSettingsCache.Store(&cachedOpenAIQuotaAutoPauseSettings{
  713. settings: cached.settings,
  714. expiresAt: 0,
  715. })
  716. }
  717. accountSchedulingThresholdsSF.Forget(SettingKeyAccountSchedulingThresholds)
  718. if settings.AccountSchedulingThresholds != nil {
  719. normalizedThresholds, err := validateAndNormalizeAccountSchedulingThresholds(settings.AccountSchedulingThresholds)
  720. if err != nil {
  721. normalizedThresholds = defaultAccountSchedulingThresholds()
  722. }
  723. accountSchedulingThresholdsCache.Store(&cachedAccountSchedulingThresholds{
  724. thresholds: cloneAccountSchedulingThresholds(normalizedThresholds),
  725. expiresAt: time.Now().Add(accountSchedulingThresholdsCacheTTL).UnixNano(),
  726. })
  727. } else {
  728. // Partial/omitted payload: clear cache so the next hot-path read reloads from DB.
  729. accountSchedulingThresholdsCache.Store(&cachedAccountSchedulingThresholds{})
  730. }
  731. if s.cfg != nil {
  732. s.cfg.SetForwardedClientIPSettings(settings.APIKeyACLTrustForwardedIP, settings.ForwardedClientIPHeaders)
  733. }
  734. // codex_cli_only 加固策略缓存:设置更新后强制下次重载(涉及 4 个键 + JSON 解析,直接置过期)。
  735. s.codexRestrictionPolicySF.Forget("codex_restriction_policy")
  736. s.codexRestrictionPolicyCache.Store(&cachedCodexRestrictionPolicy{expiresAt: 0})
  737. if s.onUpdate != nil {
  738. s.onUpdate() // Invalidate cache after settings update
  739. }
  740. s.notifyChannelMonitorRuntimeListeners()
  741. }
  742. func (s *SettingService) defaultRewriteMessageCacheControl() bool {
  743. return false
  744. }
  745. func (s *SettingService) validateDefaultSubscriptionGroups(ctx context.Context, items []DefaultSubscriptionSetting) error {
  746. if len(items) == 0 {
  747. return nil
  748. }
  749. checked := make(map[int64]struct{}, len(items))
  750. for _, item := range items {
  751. if item.GroupID <= 0 {
  752. continue
  753. }
  754. if _, ok := checked[item.GroupID]; ok {
  755. return ErrDefaultSubGroupDuplicate.WithMetadata(map[string]string{
  756. "group_id": strconv.FormatInt(item.GroupID, 10),
  757. })
  758. }
  759. checked[item.GroupID] = struct{}{}
  760. if s.defaultSubGroupReader == nil {
  761. continue
  762. }
  763. group, err := s.defaultSubGroupReader.GetByID(ctx, item.GroupID)
  764. if err != nil {
  765. if errors.Is(err, ErrGroupNotFound) {
  766. return ErrDefaultSubGroupInvalid.WithMetadata(map[string]string{
  767. "group_id": strconv.FormatInt(item.GroupID, 10),
  768. })
  769. }
  770. return fmt.Errorf("get default subscription group %d: %w", item.GroupID, err)
  771. }
  772. if !group.IsSubscriptionType() {
  773. return ErrDefaultSubGroupInvalid.WithMetadata(map[string]string{
  774. "group_id": strconv.FormatInt(item.GroupID, 10),
  775. })
  776. }
  777. }
  778. return nil
  779. }