setting_features.go 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218
  1. package service
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "encoding/hex"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "log/slog"
  10. "math"
  11. "strconv"
  12. "strings"
  13. "time"
  14. )
  15. // IsRegistrationEnabled 检查是否开放注册
  16. func (s *SettingService) IsRegistrationEnabled(ctx context.Context) bool {
  17. value, err := s.settingRepo.GetValue(ctx, SettingKeyRegistrationEnabled)
  18. if err != nil {
  19. // 安全默认:如果设置不存在或查询出错,默认关闭注册
  20. return false
  21. }
  22. return value == "true"
  23. }
  24. // IsEmailVerifyEnabled 检查是否开启邮件验证
  25. func (s *SettingService) IsEmailVerifyEnabled(ctx context.Context) bool {
  26. value, err := s.settingRepo.GetValue(ctx, SettingKeyEmailVerifyEnabled)
  27. if err != nil {
  28. return false
  29. }
  30. return value == "true"
  31. }
  32. // IsRegistrationEmailDomainQuotaEnabled 检查白名单非空时是否放行非白名单域名限量注册。
  33. // 安全默认:设置缺失或查询出错时按关闭处理(保持白名单严格模式)。
  34. func (s *SettingService) IsRegistrationEmailDomainQuotaEnabled(ctx context.Context) bool {
  35. value, err := s.settingRepo.GetValue(ctx, SettingKeyRegistrationEmailDomainQuotaEnabled)
  36. if err != nil {
  37. return false
  38. }
  39. return value == "true"
  40. }
  41. // GetRegistrationEmailSuffixWhitelist returns normalized registration email suffix whitelist.
  42. func (s *SettingService) GetRegistrationEmailSuffixWhitelist(ctx context.Context) []string {
  43. value, err := s.settingRepo.GetValue(ctx, SettingKeyRegistrationEmailSuffixWhitelist)
  44. if err != nil {
  45. return []string{}
  46. }
  47. return ParseRegistrationEmailSuffixWhitelist(value)
  48. }
  49. // IsPromoCodeEnabled 检查是否启用优惠码功能
  50. func (s *SettingService) IsPromoCodeEnabled(ctx context.Context) bool {
  51. value, err := s.settingRepo.GetValue(ctx, SettingKeyPromoCodeEnabled)
  52. if err != nil {
  53. return true // 默认启用
  54. }
  55. return value != "false"
  56. }
  57. // IsInvitationCodeEnabled 检查是否启用邀请码注册功能
  58. func (s *SettingService) IsInvitationCodeEnabled(ctx context.Context) bool {
  59. value, err := s.settingRepo.GetValue(ctx, SettingKeyInvitationCodeEnabled)
  60. if err != nil {
  61. return false // 默认关闭
  62. }
  63. return value == "true"
  64. }
  65. // GetCustomMenuItemsRaw returns the raw JSON string of custom_menu_items setting.
  66. func (s *SettingService) GetCustomMenuItemsRaw(ctx context.Context) string {
  67. value, err := s.settingRepo.GetValue(ctx, SettingKeyCustomMenuItems)
  68. if err != nil {
  69. return "[]"
  70. }
  71. return value
  72. }
  73. // IsAffiliateEnabled 检查是否启用邀请返利功能(总开关)
  74. func (s *SettingService) IsAffiliateEnabled(ctx context.Context) bool {
  75. value, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateEnabled)
  76. if err != nil {
  77. return false // 默认关闭
  78. }
  79. return value == "true"
  80. }
  81. // IsAffiliateAdminRechargeEnabled reports whether admin balance
  82. // deposits should participate in the affiliate rebate program.
  83. func (s *SettingService) IsAffiliateAdminRechargeEnabled(ctx context.Context) bool {
  84. value, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateAdminRechargeEnabled)
  85. if err != nil {
  86. return AdminRechargeRebateEnabledDefault
  87. }
  88. return value == "true"
  89. }
  90. // GetAffiliateRebateRatePercent 读取并 clamp 全局返利比例。
  91. // 解析失败、缺失或越界都回退到 AffiliateRebateRateDefault — 该比例从不抛错,
  92. // 调用方只关心一个可用的数值。
  93. func (s *SettingService) GetAffiliateRebateRatePercent(ctx context.Context) float64 {
  94. raw, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateRebateRate)
  95. if err != nil {
  96. return AffiliateRebateRateDefault
  97. }
  98. rate, err := strconv.ParseFloat(strings.TrimSpace(raw), 64)
  99. if err != nil || math.IsNaN(rate) || math.IsInf(rate, 0) {
  100. return AffiliateRebateRateDefault
  101. }
  102. return clampAffiliateRebateRate(rate)
  103. }
  104. // GetAffiliateRebateFreezeHours 返回返利冻结期(小时)。
  105. // 返回 0 表示不冻结(向后兼容)。
  106. func (s *SettingService) GetAffiliateRebateFreezeHours(ctx context.Context) int {
  107. raw, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateRebateFreezeHours)
  108. if err != nil {
  109. return AffiliateRebateFreezeHoursDefault
  110. }
  111. hours, err := strconv.Atoi(strings.TrimSpace(raw))
  112. if err != nil || hours < 0 {
  113. return AffiliateRebateFreezeHoursDefault
  114. }
  115. if hours > AffiliateRebateFreezeHoursMax {
  116. return AffiliateRebateFreezeHoursMax
  117. }
  118. return hours
  119. }
  120. // GetAffiliateRebateDurationDays 返回返利有效期(天)。
  121. // 返回 0 表示永久有效。
  122. func (s *SettingService) GetAffiliateRebateDurationDays(ctx context.Context) int {
  123. raw, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateRebateDurationDays)
  124. if err != nil {
  125. return AffiliateRebateDurationDaysDefault
  126. }
  127. days, err := strconv.Atoi(strings.TrimSpace(raw))
  128. if err != nil || days < 0 {
  129. return AffiliateRebateDurationDaysDefault
  130. }
  131. if days > AffiliateRebateDurationDaysMax {
  132. return AffiliateRebateDurationDaysMax
  133. }
  134. return days
  135. }
  136. // GetAffiliateRebatePerInviteeCap 返回单人返利上限。
  137. // 返回 0 表示无上限。
  138. func (s *SettingService) GetAffiliateRebatePerInviteeCap(ctx context.Context) float64 {
  139. raw, err := s.settingRepo.GetValue(ctx, SettingKeyAffiliateRebatePerInviteeCap)
  140. if err != nil {
  141. return AffiliateRebatePerInviteeCapDefault
  142. }
  143. cap, err := strconv.ParseFloat(strings.TrimSpace(raw), 64)
  144. if err != nil || cap < 0 || math.IsNaN(cap) || math.IsInf(cap, 0) {
  145. return AffiliateRebatePerInviteeCapDefault
  146. }
  147. return cap
  148. }
  149. // IsPasswordResetEnabled 检查是否启用密码重置功能
  150. // 要求:必须同时开启邮件验证
  151. func (s *SettingService) IsPasswordResetEnabled(ctx context.Context) bool {
  152. // Password reset requires email verification to be enabled
  153. if !s.IsEmailVerifyEnabled(ctx) {
  154. return false
  155. }
  156. value, err := s.settingRepo.GetValue(ctx, SettingKeyPasswordResetEnabled)
  157. if err != nil {
  158. return false // 默认关闭
  159. }
  160. return value == "true"
  161. }
  162. // IsTotpEnabled 检查是否启用 TOTP 双因素认证功能
  163. func (s *SettingService) IsTotpEnabled(ctx context.Context) bool {
  164. value, err := s.settingRepo.GetValue(ctx, SettingKeyTotpEnabled)
  165. if err != nil {
  166. return false // 默认关闭
  167. }
  168. return value == "true"
  169. }
  170. // PasskeyEnabled reports the effective runtime switch. WebAuthn deployment
  171. // configuration remains the security boundary; the database setting can only
  172. // disable a valid configured relying party, never replace or weaken it.
  173. func (s *SettingService) PasskeyEnabled(ctx context.Context) (bool, error) {
  174. if !s.passkeyConfigured() {
  175. return false, nil
  176. }
  177. value, err := s.settingRepo.GetValue(ctx, SettingKeyPasskeyEnabled)
  178. if errors.Is(err, ErrSettingNotFound) {
  179. return true, nil // configured deployments default to enabled until the admin persists the switch
  180. }
  181. if err != nil {
  182. return false, fmt.Errorf("read passkey setting: %w", err)
  183. }
  184. return value == "true", nil
  185. }
  186. // PasskeyConfiguration returns non-secret relying-party configuration for the
  187. // admin status UI. Enabled configurations have already passed Config.Validate.
  188. func (s *SettingService) PasskeyConfiguration() (configured bool, rpID string, origins []string) {
  189. if s == nil || s.cfg == nil {
  190. return false, "", []string{}
  191. }
  192. origins = append([]string{}, s.cfg.WebAuthn.RPOrigins...)
  193. return s.cfg.WebAuthn.Enabled,
  194. strings.TrimSpace(s.cfg.WebAuthn.RPID),
  195. origins
  196. }
  197. func (s *SettingService) passkeyConfigured() bool {
  198. return s != nil && s.cfg != nil && s.cfg.WebAuthn.Enabled
  199. }
  200. // passkeySettingEnabled must stay ANDed with passkeyConfigured: a stale
  201. // "true" row after the WebAuthn config is removed would otherwise make the
  202. // admin update gate reject every settings save while the UI toggle is locked.
  203. func (s *SettingService) passkeySettingEnabled(settings map[string]string) bool {
  204. if !s.passkeyConfigured() {
  205. return false
  206. }
  207. value, ok := settings[SettingKeyPasskeyEnabled]
  208. if !ok {
  209. return true
  210. }
  211. return value == "true"
  212. }
  213. // IsTotpEncryptionKeyConfigured 检查 TOTP 加密密钥是否已手动配置
  214. // 只有手动配置了密钥才允许在管理后台启用 TOTP 功能
  215. func (s *SettingService) IsTotpEncryptionKeyConfigured() bool {
  216. return s.cfg.Totp.EncryptionKeyConfigured
  217. }
  218. // IsSessionBindingEnabled 检查会话 IP/UA 绑定是否启用(默认关闭)。
  219. // 开启时会话与登录时的 IP/User-Agent 绑定,任一变化立即失效并撤销该会话。
  220. // 默认关闭:移动网络/多出口 IP 场景下 IP 频繁变化会导致登录后立即掉线。
  221. func (s *SettingService) IsSessionBindingEnabled(ctx context.Context) bool {
  222. value, err := s.settingRepo.GetValue(ctx, SettingKeySessionBindingEnabled)
  223. if err != nil {
  224. return false // 默认关闭
  225. }
  226. return value == "true"
  227. }
  228. // IsStepUpEnabled 检查敏感操作 step-up 2FA 门控是否启用(默认关闭)。
  229. // 开启时账号/代理导出、备份创建/下载、S3 配置修改、提升管理员等操作
  230. // 要求当前会话在有效期内完成过 TOTP step-up 验证。
  231. func (s *SettingService) IsStepUpEnabled(ctx context.Context) bool {
  232. value, err := s.settingRepo.GetValue(ctx, SettingKeyStepUpEnabled)
  233. if err != nil {
  234. return false // 默认关闭
  235. }
  236. return value == "true"
  237. }
  238. // defaultAuditLogRetentionDays 审计日志默认保留天数。
  239. const defaultAuditLogRetentionDays = 180
  240. // GetAuditLogRetentionDays 审计日志保留天数(<=0 表示永久保留,仅支持手动清空)。
  241. func (s *SettingService) GetAuditLogRetentionDays(ctx context.Context) int {
  242. value, err := s.settingRepo.GetValue(ctx, SettingKeyAuditLogRetentionDays)
  243. if err != nil {
  244. return defaultAuditLogRetentionDays
  245. }
  246. return parseAuditLogRetentionDays(value)
  247. }
  248. // parseAuditLogRetentionDays 解析保留天数配置,空/非法值回退默认值。
  249. func parseAuditLogRetentionDays(value string) int {
  250. value = strings.TrimSpace(value)
  251. if value == "" {
  252. return defaultAuditLogRetentionDays
  253. }
  254. n, err := strconv.Atoi(value)
  255. if err != nil {
  256. return defaultAuditLogRetentionDays
  257. }
  258. if n < 0 {
  259. return 0
  260. }
  261. return n
  262. }
  263. // GetSiteName 获取网站名称
  264. func (s *SettingService) GetSiteName(ctx context.Context) string {
  265. value, err := s.settingRepo.GetValue(ctx, SettingKeySiteName)
  266. if err != nil || value == "" {
  267. return "Sub2API"
  268. }
  269. return value
  270. }
  271. // GetDefaultConcurrency 获取默认并发量
  272. func (s *SettingService) GetDefaultConcurrency(ctx context.Context) int {
  273. value, err := s.settingRepo.GetValue(ctx, SettingKeyDefaultConcurrency)
  274. if err != nil {
  275. return s.cfg.Default.UserConcurrency
  276. }
  277. if v, err := strconv.Atoi(value); err == nil && v > 0 {
  278. return v
  279. }
  280. return s.cfg.Default.UserConcurrency
  281. }
  282. // GetDefaultBalance 获取默认余额
  283. func (s *SettingService) GetDefaultBalance(ctx context.Context) float64 {
  284. value, err := s.settingRepo.GetValue(ctx, SettingKeyDefaultBalance)
  285. if err != nil {
  286. return s.cfg.Default.UserBalance
  287. }
  288. if v, err := strconv.ParseFloat(value, 64); err == nil && v >= 0 {
  289. return v
  290. }
  291. return s.cfg.Default.UserBalance
  292. }
  293. // GetDefaultUserRPMLimit 获取新用户默认 RPM 限制(0 = 不限制)。未配置则返回 0。
  294. func (s *SettingService) GetDefaultUserRPMLimit(ctx context.Context) int {
  295. value, err := s.settingRepo.GetValue(ctx, SettingKeyDefaultUserRPMLimit)
  296. if err != nil || value == "" {
  297. return 0
  298. }
  299. if v, err := strconv.Atoi(value); err == nil && v >= 0 {
  300. return v
  301. }
  302. return 0
  303. }
  304. // GetDefaultSubscriptions 获取新用户默认订阅配置列表。
  305. func (s *SettingService) GetDefaultSubscriptions(ctx context.Context) []DefaultSubscriptionSetting {
  306. value, err := s.settingRepo.GetValue(ctx, SettingKeyDefaultSubscriptions)
  307. if err != nil {
  308. return nil
  309. }
  310. return parseDefaultSubscriptions(value)
  311. }
  312. func (s *SettingService) GetAuthSourceDefaultSettings(ctx context.Context) (*AuthSourceDefaultSettings, error) {
  313. keys := []string{
  314. SettingKeyAuthSourceDefaultEmailBalance,
  315. SettingKeyAuthSourceDefaultEmailConcurrency,
  316. SettingKeyAuthSourceDefaultEmailSubscriptions,
  317. SettingKeyAuthSourceDefaultEmailGrantOnSignup,
  318. SettingKeyAuthSourceDefaultEmailGrantOnFirstBind,
  319. SettingKeyAuthSourceDefaultLinuxDoBalance,
  320. SettingKeyAuthSourceDefaultLinuxDoConcurrency,
  321. SettingKeyAuthSourceDefaultLinuxDoSubscriptions,
  322. SettingKeyAuthSourceDefaultLinuxDoGrantOnSignup,
  323. SettingKeyAuthSourceDefaultLinuxDoGrantOnFirstBind,
  324. SettingKeyAuthSourceDefaultOIDCBalance,
  325. SettingKeyAuthSourceDefaultOIDCConcurrency,
  326. SettingKeyAuthSourceDefaultOIDCSubscriptions,
  327. SettingKeyAuthSourceDefaultOIDCGrantOnSignup,
  328. SettingKeyAuthSourceDefaultOIDCGrantOnFirstBind,
  329. SettingKeyAuthSourceDefaultWeChatBalance,
  330. SettingKeyAuthSourceDefaultWeChatConcurrency,
  331. SettingKeyAuthSourceDefaultWeChatSubscriptions,
  332. SettingKeyAuthSourceDefaultWeChatGrantOnSignup,
  333. SettingKeyAuthSourceDefaultWeChatGrantOnFirstBind,
  334. SettingKeyAuthSourceDefaultGitHubBalance,
  335. SettingKeyAuthSourceDefaultGitHubConcurrency,
  336. SettingKeyAuthSourceDefaultGitHubSubscriptions,
  337. SettingKeyAuthSourceDefaultGitHubGrantOnSignup,
  338. SettingKeyAuthSourceDefaultGitHubGrantOnFirstBind,
  339. SettingKeyAuthSourceDefaultGoogleBalance,
  340. SettingKeyAuthSourceDefaultGoogleConcurrency,
  341. SettingKeyAuthSourceDefaultGoogleSubscriptions,
  342. SettingKeyAuthSourceDefaultGoogleGrantOnSignup,
  343. SettingKeyAuthSourceDefaultGoogleGrantOnFirstBind,
  344. SettingKeyAuthSourceDefaultDingTalkBalance,
  345. SettingKeyAuthSourceDefaultDingTalkConcurrency,
  346. SettingKeyAuthSourceDefaultDingTalkSubscriptions,
  347. SettingKeyAuthSourceDefaultDingTalkGrantOnSignup,
  348. SettingKeyAuthSourceDefaultDingTalkGrantOnFirstBind,
  349. SettingKeyAuthSourcePlatformQuotas("email"),
  350. SettingKeyAuthSourcePlatformQuotas("linuxdo"),
  351. SettingKeyAuthSourcePlatformQuotas("oidc"),
  352. SettingKeyAuthSourcePlatformQuotas("wechat"),
  353. SettingKeyAuthSourcePlatformQuotas("github"),
  354. SettingKeyAuthSourcePlatformQuotas("google"),
  355. SettingKeyAuthSourcePlatformQuotas("dingtalk"),
  356. SettingKeyForceEmailOnThirdPartySignup,
  357. }
  358. settings, err := s.settingRepo.GetMultiple(ctx, keys)
  359. if err != nil {
  360. return nil, fmt.Errorf("get auth source default settings: %w", err)
  361. }
  362. return &AuthSourceDefaultSettings{
  363. Email: parseProviderDefaultGrantSettings(settings, emailAuthSourceDefaultKeys),
  364. LinuxDo: parseProviderDefaultGrantSettings(settings, linuxDoAuthSourceDefaultKeys),
  365. OIDC: parseProviderDefaultGrantSettings(settings, oidcAuthSourceDefaultKeys),
  366. WeChat: parseProviderDefaultGrantSettings(settings, weChatAuthSourceDefaultKeys),
  367. GitHub: parseProviderDefaultGrantSettings(settings, gitHubAuthSourceDefaultKeys),
  368. Google: parseProviderDefaultGrantSettings(settings, googleAuthSourceDefaultKeys),
  369. DingTalk: parseProviderDefaultGrantSettings(settings, dingTalkAuthSourceDefaultKeys),
  370. ForceEmailOnThirdPartySignup: settings[SettingKeyForceEmailOnThirdPartySignup] == "true",
  371. }, nil
  372. }
  373. func (s *SettingService) ResolveAuthSourceGrantSettings(ctx context.Context, signupSource string, firstBind bool) (ProviderDefaultGrantSettings, bool, error) {
  374. result := ProviderDefaultGrantSettings{
  375. Balance: s.GetDefaultBalance(ctx),
  376. Concurrency: s.GetDefaultConcurrency(ctx),
  377. Subscriptions: s.GetDefaultSubscriptions(ctx),
  378. }
  379. defaults, err := s.GetAuthSourceDefaultSettings(ctx)
  380. if err != nil {
  381. return result, false, err
  382. }
  383. providerDefaults, ok := authSourceSignupSettings(defaults, signupSource)
  384. if !ok {
  385. return result, false, nil
  386. }
  387. enabled := providerDefaults.GrantOnSignup
  388. if firstBind {
  389. enabled = providerDefaults.GrantOnFirstBind
  390. }
  391. if !enabled {
  392. return result, false, nil
  393. }
  394. return mergeProviderDefaultGrantSettings(result, providerDefaults), true, nil
  395. }
  396. func (s *SettingService) UpdateAuthSourceDefaultSettings(ctx context.Context, settings *AuthSourceDefaultSettings) error {
  397. updates, err := s.buildAuthSourceDefaultUpdates(ctx, settings)
  398. if err != nil {
  399. return err
  400. }
  401. if len(updates) == 0 {
  402. return nil
  403. }
  404. if err := s.settingRepo.SetMultiple(ctx, updates); err != nil {
  405. return fmt.Errorf("update auth source default settings: %w", err)
  406. }
  407. return nil
  408. }
  409. // IsTurnstileEnabled 检查是否启用 Turnstile 验证
  410. func (s *SettingService) IsTurnstileEnabled(ctx context.Context) bool {
  411. value, err := s.settingRepo.GetValue(ctx, SettingKeyTurnstileEnabled)
  412. if err != nil {
  413. return false
  414. }
  415. return value == "true"
  416. }
  417. // GetTurnstileSecretKey 获取 Turnstile Secret Key
  418. func (s *SettingService) GetTurnstileSecretKey(ctx context.Context) string {
  419. value, err := s.settingRepo.GetValue(ctx, SettingKeyTurnstileSecretKey)
  420. if err != nil {
  421. return ""
  422. }
  423. return value
  424. }
  425. // TencentCaptchaConfig contains the credentials required by Tencent Cloud's
  426. // ticket verification API. It must never be returned by a public handler.
  427. type TencentCaptchaConfig struct {
  428. Enabled bool
  429. AppID string
  430. AppSecretKey string
  431. CloudSecretID string
  432. CloudSecretKey string
  433. Region string
  434. }
  435. // AliyunCaptchaConfig contains the credentials required by Aliyun Captcha 2.0's
  436. // server-side verification API. It must never be returned by a public handler.
  437. type AliyunCaptchaConfig struct {
  438. Enabled bool
  439. AccessKeyID string
  440. AccessKeySecret string
  441. SceneID string
  442. Region string
  443. }
  444. type CaptchaProviderConfig struct {
  445. TurnstileEnabled bool
  446. TurnstileSecretKey string
  447. Tencent TencentCaptchaConfig
  448. Aliyun AliyunCaptchaConfig
  449. }
  450. func (s *SettingService) GetCaptchaProviderConfig(ctx context.Context) (CaptchaProviderConfig, error) {
  451. values, err := s.settingRepo.GetMultiple(ctx, []string{
  452. SettingKeyTurnstileEnabled,
  453. SettingKeyTurnstileSecretKey,
  454. SettingKeyTencentCaptchaEnabled,
  455. SettingKeyTencentCaptchaAppID,
  456. SettingKeyTencentCaptchaAppSecretKey,
  457. SettingKeyTencentCaptchaCloudSecretID,
  458. SettingKeyTencentCaptchaCloudSecretKey,
  459. SettingKeyTencentCaptchaRegion,
  460. SettingKeyAliyunCaptchaEnabled,
  461. SettingKeyAliyunCaptchaAccessKeyID,
  462. SettingKeyAliyunCaptchaAccessKeySecret,
  463. SettingKeyAliyunCaptchaSceneID,
  464. SettingKeyAliyunCaptchaRegion,
  465. })
  466. if err != nil {
  467. return CaptchaProviderConfig{}, fmt.Errorf("read captcha provider settings: %w", err)
  468. }
  469. return CaptchaProviderConfig{
  470. TurnstileEnabled: values[SettingKeyTurnstileEnabled] == "true",
  471. TurnstileSecretKey: values[SettingKeyTurnstileSecretKey],
  472. Tencent: TencentCaptchaConfig{
  473. Enabled: values[SettingKeyTencentCaptchaEnabled] == "true",
  474. AppID: values[SettingKeyTencentCaptchaAppID],
  475. AppSecretKey: values[SettingKeyTencentCaptchaAppSecretKey],
  476. CloudSecretID: values[SettingKeyTencentCaptchaCloudSecretID],
  477. CloudSecretKey: values[SettingKeyTencentCaptchaCloudSecretKey],
  478. Region: normalizeTencentCaptchaRegion(values[SettingKeyTencentCaptchaRegion]),
  479. },
  480. Aliyun: AliyunCaptchaConfig{
  481. Enabled: values[SettingKeyAliyunCaptchaEnabled] == "true",
  482. AccessKeyID: values[SettingKeyAliyunCaptchaAccessKeyID],
  483. AccessKeySecret: values[SettingKeyAliyunCaptchaAccessKeySecret],
  484. SceneID: values[SettingKeyAliyunCaptchaSceneID],
  485. Region: normalizeAliyunCaptchaRegion(values[SettingKeyAliyunCaptchaRegion]),
  486. },
  487. }, nil
  488. }
  489. func (s *SettingService) IsTencentCaptchaEnabled(ctx context.Context) bool {
  490. value, err := s.settingRepo.GetValue(ctx, SettingKeyTencentCaptchaEnabled)
  491. return err == nil && value == "true"
  492. }
  493. func (s *SettingService) GetTencentCaptchaConfig(ctx context.Context) TencentCaptchaConfig {
  494. config, err := s.GetCaptchaProviderConfig(ctx)
  495. if err != nil {
  496. return TencentCaptchaConfig{}
  497. }
  498. return config.Tencent
  499. }
  500. // IsIdentityPatchEnabled 检查是否启用身份补丁(Claude -> Gemini systemInstruction 注入)
  501. func (s *SettingService) IsIdentityPatchEnabled(ctx context.Context) bool {
  502. value, err := s.settingRepo.GetValue(ctx, SettingKeyEnableIdentityPatch)
  503. if err != nil {
  504. // 默认开启,保持兼容
  505. return true
  506. }
  507. return value == "true"
  508. }
  509. // GetIdentityPatchPrompt 获取自定义身份补丁提示词(为空表示使用内置默认模板)
  510. func (s *SettingService) GetIdentityPatchPrompt(ctx context.Context) string {
  511. value, err := s.settingRepo.GetValue(ctx, SettingKeyIdentityPatchPrompt)
  512. if err != nil {
  513. return ""
  514. }
  515. return value
  516. }
  517. // GenerateAdminAPIKey 生成新的管理员 API Key
  518. func (s *SettingService) GenerateAdminAPIKey(ctx context.Context) (string, error) {
  519. // 生成 32 字节随机数 = 64 位十六进制字符
  520. bytes := make([]byte, 32)
  521. if _, err := rand.Read(bytes); err != nil {
  522. return "", fmt.Errorf("generate random bytes: %w", err)
  523. }
  524. key := AdminAPIKeyPrefix + hex.EncodeToString(bytes)
  525. // 存储到 settings 表
  526. if err := s.settingRepo.Set(ctx, SettingKeyAdminAPIKey, key); err != nil {
  527. return "", fmt.Errorf("save admin api key: %w", err)
  528. }
  529. return key, nil
  530. }
  531. // GetAdminAPIKeyStatus 获取管理员 API Key 状态
  532. // 返回脱敏的 key、是否存在、错误
  533. func (s *SettingService) GetAdminAPIKeyStatus(ctx context.Context) (maskedKey string, exists bool, err error) {
  534. key, err := s.settingRepo.GetValue(ctx, SettingKeyAdminAPIKey)
  535. if err != nil {
  536. if errors.Is(err, ErrSettingNotFound) {
  537. return "", false, nil
  538. }
  539. return "", false, err
  540. }
  541. if key == "" {
  542. return "", false, nil
  543. }
  544. // 脱敏:显示前 10 位和后 4 位
  545. if len(key) > 14 {
  546. maskedKey = key[:10] + "..." + key[len(key)-4:]
  547. } else {
  548. maskedKey = key
  549. }
  550. return maskedKey, true, nil
  551. }
  552. // GetAdminAPIKey 获取完整的管理员 API Key(仅供内部验证使用)
  553. // 如果未配置返回空字符串和 nil 错误,只有数据库错误时才返回 error
  554. func (s *SettingService) GetAdminAPIKey(ctx context.Context) (string, error) {
  555. key, err := s.settingRepo.GetValue(ctx, SettingKeyAdminAPIKey)
  556. if err != nil {
  557. if errors.Is(err, ErrSettingNotFound) {
  558. return "", nil // 未配置,返回空字符串
  559. }
  560. return "", err // 数据库错误
  561. }
  562. return key, nil
  563. }
  564. // DeleteAdminAPIKey 删除管理员 API Key
  565. func (s *SettingService) DeleteAdminAPIKey(ctx context.Context) error {
  566. return s.settingRepo.Delete(ctx, SettingKeyAdminAPIKey)
  567. }
  568. // IsModelFallbackEnabled 检查是否启用模型兜底机制
  569. func (s *SettingService) IsModelFallbackEnabled(ctx context.Context) bool {
  570. value, err := s.settingRepo.GetValue(ctx, SettingKeyEnableModelFallback)
  571. if err != nil {
  572. return false // Default: disabled
  573. }
  574. return value == "true"
  575. }
  576. // GetFallbackModel 获取指定平台的兜底模型
  577. func (s *SettingService) GetFallbackModel(ctx context.Context, platform string) string {
  578. var key string
  579. var defaultModel string
  580. switch platform {
  581. case PlatformAnthropic:
  582. key = SettingKeyFallbackModelAnthropic
  583. defaultModel = "claude-3-5-sonnet-20241022"
  584. case PlatformOpenAI:
  585. key = SettingKeyFallbackModelOpenAI
  586. defaultModel = "gpt-4o"
  587. case PlatformGemini:
  588. key = SettingKeyFallbackModelGemini
  589. defaultModel = "gemini-2.5-pro"
  590. case PlatformAntigravity:
  591. key = SettingKeyFallbackModelAntigravity
  592. defaultModel = "gemini-2.5-pro"
  593. default:
  594. return ""
  595. }
  596. value, err := s.settingRepo.GetValue(ctx, key)
  597. if err != nil || value == "" {
  598. return defaultModel
  599. }
  600. return value
  601. }
  602. // GetOverloadCooldownSettings 获取529过载冷却配置
  603. func (s *SettingService) GetOverloadCooldownSettings(ctx context.Context) (*OverloadCooldownSettings, error) {
  604. value, err := s.settingRepo.GetValue(ctx, SettingKeyOverloadCooldownSettings)
  605. if err != nil {
  606. if errors.Is(err, ErrSettingNotFound) {
  607. return DefaultOverloadCooldownSettings(), nil
  608. }
  609. return nil, fmt.Errorf("get overload cooldown settings: %w", err)
  610. }
  611. if value == "" {
  612. return DefaultOverloadCooldownSettings(), nil
  613. }
  614. var settings OverloadCooldownSettings
  615. if err := json.Unmarshal([]byte(value), &settings); err != nil {
  616. return DefaultOverloadCooldownSettings(), nil
  617. }
  618. // 修正配置值范围
  619. if settings.CooldownMinutes < 1 {
  620. settings.CooldownMinutes = 1
  621. }
  622. if settings.CooldownMinutes > 120 {
  623. settings.CooldownMinutes = 120
  624. }
  625. return &settings, nil
  626. }
  627. // SetOverloadCooldownSettings 设置529过载冷却配置
  628. func (s *SettingService) SetOverloadCooldownSettings(ctx context.Context, settings *OverloadCooldownSettings) error {
  629. if settings == nil {
  630. return fmt.Errorf("settings cannot be nil")
  631. }
  632. // 禁用时修正为合法值即可,不拒绝请求
  633. if settings.CooldownMinutes < 1 || settings.CooldownMinutes > 120 {
  634. if settings.Enabled {
  635. return fmt.Errorf("cooldown_minutes must be between 1-120")
  636. }
  637. settings.CooldownMinutes = 10 // 禁用状态下归一化为默认值
  638. }
  639. data, err := json.Marshal(settings)
  640. if err != nil {
  641. return fmt.Errorf("marshal overload cooldown settings: %w", err)
  642. }
  643. return s.settingRepo.Set(ctx, SettingKeyOverloadCooldownSettings, string(data))
  644. }
  645. // GetRateLimit429CooldownSettings 获取429默认回避配置
  646. func (s *SettingService) GetRateLimit429CooldownSettings(ctx context.Context) (*RateLimit429CooldownSettings, error) {
  647. value, err := s.settingRepo.GetValue(ctx, SettingKeyRateLimit429CooldownSettings)
  648. if err != nil {
  649. if errors.Is(err, ErrSettingNotFound) {
  650. return DefaultRateLimit429CooldownSettings(), nil
  651. }
  652. return nil, fmt.Errorf("get 429 cooldown settings: %w", err)
  653. }
  654. if value == "" {
  655. return DefaultRateLimit429CooldownSettings(), nil
  656. }
  657. var settings RateLimit429CooldownSettings
  658. if err := json.Unmarshal([]byte(value), &settings); err != nil {
  659. return DefaultRateLimit429CooldownSettings(), nil
  660. }
  661. if settings.CooldownSeconds < 1 {
  662. settings.CooldownSeconds = 1
  663. }
  664. if settings.CooldownSeconds > 7200 {
  665. settings.CooldownSeconds = 7200
  666. }
  667. return &settings, nil
  668. }
  669. // SetRateLimit429CooldownSettings 设置429默认回避配置
  670. func (s *SettingService) SetRateLimit429CooldownSettings(ctx context.Context, settings *RateLimit429CooldownSettings) error {
  671. if settings == nil {
  672. return fmt.Errorf("settings cannot be nil")
  673. }
  674. if settings.CooldownSeconds < 1 || settings.CooldownSeconds > 7200 {
  675. if settings.Enabled {
  676. return fmt.Errorf("cooldown_seconds must be between 1-7200")
  677. }
  678. settings.CooldownSeconds = 5
  679. }
  680. data, err := json.Marshal(settings)
  681. if err != nil {
  682. return fmt.Errorf("marshal 429 cooldown settings: %w", err)
  683. }
  684. return s.settingRepo.Set(ctx, SettingKeyRateLimit429CooldownSettings, string(data))
  685. }
  686. func (s *SettingService) GetOpenAIImagesOAuthUnavailableCooldownSettings(ctx context.Context) (*OpenAIImagesOAuthUnavailableCooldownSettings, error) {
  687. value, err := s.settingRepo.GetValue(ctx, SettingKeyOpenAIImagesOAuthUnavailableCooldownSettings)
  688. if err != nil {
  689. if errors.Is(err, ErrSettingNotFound) {
  690. return DefaultOpenAIImagesOAuthUnavailableCooldownSettings(), nil
  691. }
  692. return nil, fmt.Errorf("get OpenAI images OAuth unavailable cooldown settings: %w", err)
  693. }
  694. if value == "" {
  695. return DefaultOpenAIImagesOAuthUnavailableCooldownSettings(), nil
  696. }
  697. var settings OpenAIImagesOAuthUnavailableCooldownSettings
  698. if err := json.Unmarshal([]byte(value), &settings); err != nil ||
  699. settings.CooldownMinutes <= 0 || settings.CooldownMinutes > openAIImagesOAuthUnavailableMaxCooldownMinutes {
  700. return DefaultOpenAIImagesOAuthUnavailableCooldownSettings(), nil
  701. }
  702. return &settings, nil
  703. }
  704. func (s *SettingService) SetOpenAIImagesOAuthUnavailableCooldownSettings(ctx context.Context, settings *OpenAIImagesOAuthUnavailableCooldownSettings) error {
  705. if settings == nil {
  706. return fmt.Errorf("settings cannot be nil")
  707. }
  708. if settings.CooldownMinutes <= 0 || settings.CooldownMinutes > openAIImagesOAuthUnavailableMaxCooldownMinutes {
  709. return fmt.Errorf("cooldown_minutes must be between 1-%d", openAIImagesOAuthUnavailableMaxCooldownMinutes)
  710. }
  711. data, err := json.Marshal(settings)
  712. if err != nil {
  713. return fmt.Errorf("marshal OpenAI images OAuth unavailable cooldown settings: %w", err)
  714. }
  715. return s.settingRepo.Set(ctx, SettingKeyOpenAIImagesOAuthUnavailableCooldownSettings, string(data))
  716. }
  717. // GetStreamTimeoutSettings 获取流超时处理配置
  718. func (s *SettingService) GetStreamTimeoutSettings(ctx context.Context) (*StreamTimeoutSettings, error) {
  719. value, err := s.settingRepo.GetValue(ctx, SettingKeyStreamTimeoutSettings)
  720. if err != nil {
  721. if errors.Is(err, ErrSettingNotFound) {
  722. return DefaultStreamTimeoutSettings(), nil
  723. }
  724. return nil, fmt.Errorf("get stream timeout settings: %w", err)
  725. }
  726. if value == "" {
  727. return DefaultStreamTimeoutSettings(), nil
  728. }
  729. var settings StreamTimeoutSettings
  730. if err := json.Unmarshal([]byte(value), &settings); err != nil {
  731. return DefaultStreamTimeoutSettings(), nil
  732. }
  733. // 验证并修正配置值
  734. if settings.TempUnschedMinutes < 1 {
  735. settings.TempUnschedMinutes = 1
  736. }
  737. if settings.TempUnschedMinutes > 60 {
  738. settings.TempUnschedMinutes = 60
  739. }
  740. if settings.ThresholdCount < 1 {
  741. settings.ThresholdCount = 1
  742. }
  743. if settings.ThresholdCount > 10 {
  744. settings.ThresholdCount = 10
  745. }
  746. if settings.ThresholdWindowMinutes < 1 {
  747. settings.ThresholdWindowMinutes = 1
  748. }
  749. if settings.ThresholdWindowMinutes > 60 {
  750. settings.ThresholdWindowMinutes = 60
  751. }
  752. // 验证 action
  753. switch settings.Action {
  754. case StreamTimeoutActionTempUnsched, StreamTimeoutActionError, StreamTimeoutActionNone:
  755. // valid
  756. default:
  757. settings.Action = StreamTimeoutActionTempUnsched
  758. }
  759. return &settings, nil
  760. }
  761. // IsUngroupedKeySchedulingAllowed 查询是否允许未分组 Key 调度
  762. func (s *SettingService) IsUngroupedKeySchedulingAllowed(ctx context.Context) bool {
  763. value, err := s.settingRepo.GetValue(ctx, SettingKeyAllowUngroupedKeyScheduling)
  764. if err != nil {
  765. return false // fail-closed: 查询失败时默认不允许
  766. }
  767. return value == "true"
  768. }
  769. // GetRectifierSettings 获取请求整流器配置
  770. func (s *SettingService) GetRectifierSettings(ctx context.Context) (*RectifierSettings, error) {
  771. value, err := s.settingRepo.GetValue(ctx, SettingKeyRectifierSettings)
  772. if err != nil {
  773. if errors.Is(err, ErrSettingNotFound) {
  774. return DefaultRectifierSettings(), nil
  775. }
  776. return nil, fmt.Errorf("get rectifier settings: %w", err)
  777. }
  778. if value == "" {
  779. return DefaultRectifierSettings(), nil
  780. }
  781. var settings RectifierSettings
  782. if err := json.Unmarshal([]byte(value), &settings); err != nil {
  783. return DefaultRectifierSettings(), nil
  784. }
  785. return &settings, nil
  786. }
  787. // SetRectifierSettings 设置请求整流器配置
  788. func (s *SettingService) SetRectifierSettings(ctx context.Context, settings *RectifierSettings) error {
  789. if settings == nil {
  790. return fmt.Errorf("settings cannot be nil")
  791. }
  792. data, err := json.Marshal(settings)
  793. if err != nil {
  794. return fmt.Errorf("marshal rectifier settings: %w", err)
  795. }
  796. return s.settingRepo.Set(ctx, SettingKeyRectifierSettings, string(data))
  797. }
  798. // IsSignatureRectifierEnabled 判断签名整流是否启用(总开关 && 签名子开关)
  799. func (s *SettingService) IsSignatureRectifierEnabled(ctx context.Context) bool {
  800. settings, err := s.GetRectifierSettings(ctx)
  801. if err != nil {
  802. return true // fail-open: 查询失败时默认启用
  803. }
  804. return settings.Enabled && settings.ThinkingSignatureEnabled
  805. }
  806. // IsBudgetRectifierEnabled 判断 Budget 整流是否启用(总开关 && Budget 子开关)
  807. func (s *SettingService) IsBudgetRectifierEnabled(ctx context.Context) bool {
  808. settings, err := s.GetRectifierSettings(ctx)
  809. if err != nil {
  810. return true // fail-open: 查询失败时默认启用
  811. }
  812. return settings.Enabled && settings.ThinkingBudgetEnabled
  813. }
  814. // GetBetaPolicySettings 获取 Beta 策略配置
  815. func (s *SettingService) GetBetaPolicySettings(ctx context.Context) (*BetaPolicySettings, error) {
  816. value, err := s.settingRepo.GetValue(ctx, SettingKeyBetaPolicySettings)
  817. if err != nil {
  818. if errors.Is(err, ErrSettingNotFound) {
  819. return DefaultBetaPolicySettings(), nil
  820. }
  821. return nil, fmt.Errorf("get beta policy settings: %w", err)
  822. }
  823. if value == "" {
  824. return DefaultBetaPolicySettings(), nil
  825. }
  826. var settings BetaPolicySettings
  827. if err := json.Unmarshal([]byte(value), &settings); err != nil {
  828. return DefaultBetaPolicySettings(), nil
  829. }
  830. return &settings, nil
  831. }
  832. // SetBetaPolicySettings 设置 Beta 策略配置
  833. func (s *SettingService) SetBetaPolicySettings(ctx context.Context, settings *BetaPolicySettings) error {
  834. if settings == nil {
  835. return fmt.Errorf("settings cannot be nil")
  836. }
  837. validActions := map[string]bool{
  838. BetaPolicyActionPass: true, BetaPolicyActionFilter: true, BetaPolicyActionBlock: true,
  839. }
  840. validScopes := map[string]bool{
  841. BetaPolicyScopeAll: true, BetaPolicyScopeOAuth: true, BetaPolicyScopeAPIKey: true, BetaPolicyScopeBedrock: true,
  842. }
  843. for i, rule := range settings.Rules {
  844. if rule.BetaToken == "" {
  845. return fmt.Errorf("rule[%d]: beta_token cannot be empty", i)
  846. }
  847. if !validActions[rule.Action] {
  848. return fmt.Errorf("rule[%d]: invalid action %q", i, rule.Action)
  849. }
  850. if !validScopes[rule.Scope] {
  851. return fmt.Errorf("rule[%d]: invalid scope %q", i, rule.Scope)
  852. }
  853. // Validate model_whitelist patterns
  854. for j, pattern := range rule.ModelWhitelist {
  855. trimmed := strings.TrimSpace(pattern)
  856. if trimmed == "" {
  857. return fmt.Errorf("rule[%d]: model_whitelist[%d] cannot be empty", i, j)
  858. }
  859. settings.Rules[i].ModelWhitelist[j] = trimmed
  860. }
  861. // Validate fallback_action
  862. if rule.FallbackAction != "" && !validActions[rule.FallbackAction] {
  863. return fmt.Errorf("rule[%d]: invalid fallback_action %q", i, rule.FallbackAction)
  864. }
  865. }
  866. data, err := json.Marshal(settings)
  867. if err != nil {
  868. return fmt.Errorf("marshal beta policy settings: %w", err)
  869. }
  870. return s.settingRepo.Set(ctx, SettingKeyBetaPolicySettings, string(data))
  871. }
  872. // GetOpenAIFastPolicySettings 获取 OpenAI fast 策略配置
  873. func (s *SettingService) GetOpenAIFastPolicySettings(ctx context.Context) (*OpenAIFastPolicySettings, error) {
  874. value, err := s.settingRepo.GetValue(ctx, SettingKeyOpenAIFastPolicySettings)
  875. if err != nil {
  876. if errors.Is(err, ErrSettingNotFound) {
  877. return DefaultOpenAIFastPolicySettings(), nil
  878. }
  879. return nil, fmt.Errorf("get openai fast policy settings: %w", err)
  880. }
  881. if value == "" {
  882. return DefaultOpenAIFastPolicySettings(), nil
  883. }
  884. var settings OpenAIFastPolicySettings
  885. if err := json.Unmarshal([]byte(value), &settings); err != nil {
  886. // JSON 损坏时静默 fallback 到默认配置会让策略意外失效(管理员配
  887. // 置的 block/filter 规则被忽略)。记录 Warn 让运维能在出现异常
  888. // 行为时定位到 settings 表里的脏数据。
  889. slog.Warn("failed to unmarshal openai fast policy settings, falling back to defaults",
  890. "error", err,
  891. "key", SettingKeyOpenAIFastPolicySettings)
  892. return DefaultOpenAIFastPolicySettings(), nil
  893. }
  894. return &settings, nil
  895. }
  896. // SetOpenAIFastPolicySettings 设置 OpenAI fast 策略配置
  897. func (s *SettingService) SetOpenAIFastPolicySettings(ctx context.Context, settings *OpenAIFastPolicySettings) error {
  898. if settings == nil {
  899. return fmt.Errorf("settings cannot be nil")
  900. }
  901. validActions := map[string]bool{
  902. BetaPolicyActionPass: true, BetaPolicyActionFilter: true, BetaPolicyActionBlock: true,
  903. OpenAIFastPolicyActionForcePriority: true,
  904. }
  905. validScopes := map[string]bool{
  906. BetaPolicyScopeAll: true, BetaPolicyScopeOAuth: true, BetaPolicyScopeAPIKey: true, BetaPolicyScopeBedrock: true,
  907. }
  908. validTiers := map[string]bool{
  909. OpenAIFastTierAny: true, OpenAIFastTierPriority: true, OpenAIFastTierUltrafast: true, OpenAIFastTierFlex: true,
  910. }
  911. for i, rule := range settings.Rules {
  912. tier := strings.ToLower(strings.TrimSpace(rule.ServiceTier))
  913. if tier == "" {
  914. tier = OpenAIFastTierAny
  915. }
  916. if !validTiers[tier] {
  917. return fmt.Errorf("rule[%d]: invalid service_tier %q", i, rule.ServiceTier)
  918. }
  919. settings.Rules[i].ServiceTier = tier
  920. if !validActions[rule.Action] {
  921. return fmt.Errorf("rule[%d]: invalid action %q", i, rule.Action)
  922. }
  923. if !validScopes[rule.Scope] {
  924. return fmt.Errorf("rule[%d]: invalid scope %q", i, rule.Scope)
  925. }
  926. seenUserIDs := make(map[int64]struct{}, len(rule.UserIDs))
  927. for j, userID := range rule.UserIDs {
  928. if userID <= 0 {
  929. return fmt.Errorf("rule[%d]: user_ids[%d] must be positive", i, j)
  930. }
  931. if _, exists := seenUserIDs[userID]; exists {
  932. return fmt.Errorf("rule[%d]: user_ids[%d] duplicates user_id %d", i, j, userID)
  933. }
  934. seenUserIDs[userID] = struct{}{}
  935. }
  936. for j, pattern := range rule.ModelWhitelist {
  937. trimmed := strings.TrimSpace(pattern)
  938. if trimmed == "" {
  939. return fmt.Errorf("rule[%d]: model_whitelist[%d] cannot be empty", i, j)
  940. }
  941. settings.Rules[i].ModelWhitelist[j] = trimmed
  942. }
  943. if rule.FallbackAction != "" && !validActions[rule.FallbackAction] {
  944. return fmt.Errorf("rule[%d]: invalid fallback_action %q", i, rule.FallbackAction)
  945. }
  946. }
  947. data, err := json.Marshal(settings)
  948. if err != nil {
  949. return fmt.Errorf("marshal openai fast policy settings: %w", err)
  950. }
  951. return s.settingRepo.Set(ctx, SettingKeyOpenAIFastPolicySettings, string(data))
  952. }
  953. // SetStreamTimeoutSettings 设置流超时处理配置
  954. func (s *SettingService) SetStreamTimeoutSettings(ctx context.Context, settings *StreamTimeoutSettings) error {
  955. if settings == nil {
  956. return fmt.Errorf("settings cannot be nil")
  957. }
  958. // 验证配置值
  959. if settings.TempUnschedMinutes < 1 || settings.TempUnschedMinutes > 60 {
  960. return fmt.Errorf("temp_unsched_minutes must be between 1-60")
  961. }
  962. if settings.ThresholdCount < 1 || settings.ThresholdCount > 10 {
  963. return fmt.Errorf("threshold_count must be between 1-10")
  964. }
  965. if settings.ThresholdWindowMinutes < 1 || settings.ThresholdWindowMinutes > 60 {
  966. return fmt.Errorf("threshold_window_minutes must be between 1-60")
  967. }
  968. switch settings.Action {
  969. case StreamTimeoutActionTempUnsched, StreamTimeoutActionError, StreamTimeoutActionNone:
  970. // valid
  971. default:
  972. return fmt.Errorf("invalid action: %s", settings.Action)
  973. }
  974. data, err := json.Marshal(settings)
  975. if err != nil {
  976. return fmt.Errorf("marshal stream timeout settings: %w", err)
  977. }
  978. return s.settingRepo.Set(ctx, SettingKeyStreamTimeoutSettings, string(data))
  979. }
  980. // GetDefaultPlatformQuotas 读取系统全局 platform quota JSON key,返回全部允许平台 x 3 window 的设置。
  981. // 永远返回包含全部允许 platform key 的 map(值可能为零值/nil 字段,表示"上层未配置 = 不限制")。
  982. //
  983. // 使用单个 JSON key(default_platform_quotas),一次 DB roundtrip,消除旧 12-KV 格式的 N+1 问题。
  984. // 容错语义:取值失败或 unmarshal 失败 → 返回补齐全部允许平台 key 的空 map(fail-open,注册不被阻断)。
  985. func (s *SettingService) GetDefaultPlatformQuotas(ctx context.Context) (map[string]*DefaultPlatformQuotaSetting, error) {
  986. out := make(map[string]*DefaultPlatformQuotaSetting, len(AllowedQuotaPlatforms))
  987. for _, platform := range AllowedQuotaPlatforms {
  988. out[platform] = &DefaultPlatformQuotaSetting{}
  989. }
  990. raw, err := s.settingRepo.GetValue(ctx, SettingKeyDefaultPlatformQuotas)
  991. if err != nil || raw == "" {
  992. return out, nil // 无配置 = 全部不限制
  993. }
  994. parsed := map[string]*DefaultPlatformQuotaSetting{}
  995. if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
  996. slog.Warn("[Setting] unmarshal default_platform_quotas failed (fail-open)", "error", err)
  997. return out, nil
  998. }
  999. for _, platform := range AllowedQuotaPlatforms {
  1000. if v := parsed[platform]; v != nil {
  1001. out[platform] = v
  1002. }
  1003. }
  1004. return out, nil // 补齐全部允许 platform key,保持与旧实现一致的下游契约
  1005. }
  1006. // GetAccountSchedulingThresholds returns per-platform auto-pause thresholds (1..100).
  1007. // 100 disables the threshold for that platform. Hot-path cached with singleflight.
  1008. func (s *SettingService) GetAccountSchedulingThresholds(ctx context.Context) map[string]int {
  1009. if s == nil || s.settingRepo == nil {
  1010. return defaultAccountSchedulingThresholds()
  1011. }
  1012. if cached, ok := accountSchedulingThresholdsCache.Load().(*cachedAccountSchedulingThresholds); ok {
  1013. if cached != nil && len(cached.thresholds) > 0 && time.Now().UnixNano() < cached.expiresAt {
  1014. return cloneAccountSchedulingThresholds(cached.thresholds)
  1015. }
  1016. }
  1017. result, err, _ := accountSchedulingThresholdsSF.Do(SettingKeyAccountSchedulingThresholds, func() (any, error) {
  1018. if cached, ok := accountSchedulingThresholdsCache.Load().(*cachedAccountSchedulingThresholds); ok {
  1019. if cached != nil && len(cached.thresholds) > 0 && time.Now().UnixNano() < cached.expiresAt {
  1020. return cloneAccountSchedulingThresholds(cached.thresholds), nil
  1021. }
  1022. }
  1023. thresholds := defaultAccountSchedulingThresholds()
  1024. dbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), accountSchedulingThresholdsDBTimeout)
  1025. defer cancel()
  1026. raw, err := s.settingRepo.GetValue(dbCtx, SettingKeyAccountSchedulingThresholds)
  1027. if err != nil {
  1028. if errors.Is(err, ErrSettingNotFound) {
  1029. accountSchedulingThresholdsCache.Store(&cachedAccountSchedulingThresholds{
  1030. thresholds: cloneAccountSchedulingThresholds(thresholds),
  1031. expiresAt: time.Now().Add(accountSchedulingThresholdsCacheTTL).UnixNano(),
  1032. })
  1033. return cloneAccountSchedulingThresholds(thresholds), nil
  1034. }
  1035. slog.Warn("failed to get account scheduling thresholds, falling back to defaults", "error", err)
  1036. accountSchedulingThresholdsCache.Store(&cachedAccountSchedulingThresholds{
  1037. thresholds: cloneAccountSchedulingThresholds(thresholds),
  1038. expiresAt: time.Now().Add(accountSchedulingThresholdsErrorTTL).UnixNano(),
  1039. })
  1040. return cloneAccountSchedulingThresholds(thresholds), nil
  1041. }
  1042. if trimmed := strings.TrimSpace(raw); trimmed != "" {
  1043. if parsed, err := parseAccountSchedulingThresholdsSetting(trimmed); err != nil {
  1044. slog.Warn("failed to parse account scheduling thresholds, falling back to defaults", "error", err)
  1045. } else {
  1046. thresholds = parsed
  1047. }
  1048. }
  1049. accountSchedulingThresholdsCache.Store(&cachedAccountSchedulingThresholds{
  1050. thresholds: cloneAccountSchedulingThresholds(thresholds),
  1051. expiresAt: time.Now().Add(accountSchedulingThresholdsCacheTTL).UnixNano(),
  1052. })
  1053. return cloneAccountSchedulingThresholds(thresholds), nil
  1054. })
  1055. if err != nil {
  1056. return defaultAccountSchedulingThresholds()
  1057. }
  1058. if thresholds, ok := result.(map[string]int); ok {
  1059. return cloneAccountSchedulingThresholds(thresholds)
  1060. }
  1061. return defaultAccountSchedulingThresholds()
  1062. }
  1063. // GetAuthSourcePlatformQuotas 读取指定 auth source 的 platform quota 覆盖(仅返回有配置的平台,override 语义)。
  1064. func (s *SettingService) GetAuthSourcePlatformQuotas(ctx context.Context, source string) map[string]*DefaultPlatformQuotaSetting {
  1065. out := map[string]*DefaultPlatformQuotaSetting{}
  1066. raw, err := s.settingRepo.GetValue(ctx, SettingKeyAuthSourcePlatformQuotas(source))
  1067. if err != nil || raw == "" {
  1068. return out // 无 override
  1069. }
  1070. if err := json.Unmarshal([]byte(raw), &out); err != nil {
  1071. slog.Warn("[Setting] unmarshal auth source platform quotas failed (fail-open)", "source", source, "error", err)
  1072. return map[string]*DefaultPlatformQuotaSetting{}
  1073. }
  1074. return out // 仅含已配置平台,保持 override 语义
  1075. }
  1076. // mergePlatformQuotaDefaults 按字段级 patch:src 中非 nil 字段覆盖 dst。
  1077. // 区分 nil("未配置",保留 dst)vs &0.0("显式禁用",覆盖 dst 为 0)
  1078. func mergePlatformQuotaDefaults(dst, src *DefaultPlatformQuotaSetting) {
  1079. if src == nil || dst == nil {
  1080. return
  1081. }
  1082. if src.DailyLimitUSD != nil {
  1083. dst.DailyLimitUSD = src.DailyLimitUSD
  1084. }
  1085. if src.WeeklyLimitUSD != nil {
  1086. dst.WeeklyLimitUSD = src.WeeklyLimitUSD
  1087. }
  1088. if src.MonthlyLimitUSD != nil {
  1089. dst.MonthlyLimitUSD = src.MonthlyLimitUSD
  1090. }
  1091. }