account_protection.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. package service
  2. import (
  3. "context"
  4. "maps"
  5. "time"
  6. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  7. )
  8. const (
  9. AntiDegradationExtraKey = "anti_degradation"
  10. ProtectionScopeExtraKey = "protection_scope"
  11. )
  12. // AntiDegradationEnabled preserves the meaning of old rows. Absence of the new
  13. // flag is never interpreted as permission to change an existing account.
  14. func (a *Account) AntiDegradationEnabled() bool {
  15. if a == nil {
  16. return false
  17. }
  18. if enabled, ok := a.Extra[AntiDegradationExtraKey].(bool); ok {
  19. return enabled
  20. }
  21. return antiDegradeEnabled(a)
  22. }
  23. func (a *Account) ProtectionScope() string {
  24. if !a.AntiDegradationEnabled() {
  25. return "disabled"
  26. }
  27. if isMode1ProtectionEnabled(a) {
  28. return "codex_v3"
  29. }
  30. if a.Extra[ProtectionScopeExtraKey] == "generic_v1" {
  31. return "generic_v1"
  32. }
  33. return "legacy"
  34. }
  35. // ProtectionMode returns the concrete strategy currently persisted on the
  36. // account. Unlike ProtectionScope (which is a coarse capability bucket), this
  37. // value is suitable for admin UI/audit so operators can tell mode1, mode2 and
  38. // the explicit sub2初代 legacy strategy apart.
  39. func (a *Account) ProtectionMode() string {
  40. if a == nil || !a.AntiDegradationEnabled() {
  41. return "disabled"
  42. }
  43. if isMode1ProtectionEnabled(a) {
  44. return string(AntiDegradeMode1)
  45. }
  46. if isLegacyProtectionEnabled(a) {
  47. return string(AntiDegradeModeLegacy)
  48. }
  49. if marker, ok := a.Extra[AntiDegradeMarkerExtraKey].(map[string]any); ok {
  50. if mode, ok := marker["mode"].(string); ok && mode != "" {
  51. return mode
  52. }
  53. }
  54. return string(AntiDegradeMode2)
  55. }
  56. // PrepareNewAccountProtection is the final, server-owned default for every
  57. // creation path, including sync, import, duplicate and credential shadows.
  58. // Unsupported providers retain their native protocol and TLS settings.
  59. func PrepareNewAccountProtection(a *Account) {
  60. if a == nil {
  61. return
  62. }
  63. a.Extra = maps.Clone(a.Extra)
  64. if a.Extra == nil {
  65. a.Extra = map[string]any{}
  66. }
  67. delete(a.Extra, AntiDegradeMarkerExtraKey)
  68. delete(a.Extra, AntiDegradationExtraKey)
  69. delete(a.Extra, ProtectionScopeExtraKey)
  70. delete(a.Extra, codexFingerprintSeedExtraKey)
  71. configureAccountProtection(a)
  72. }
  73. func configureAccountProtection(a *Account) {
  74. a.Extra = maps.Clone(a.Extra)
  75. if a.Extra == nil {
  76. a.Extra = map[string]any{}
  77. }
  78. a.Extra[AntiDegradationExtraKey] = true
  79. cap := a.Concurrency
  80. if cap <= 0 {
  81. cap = AntiDegradeConcurrencyCap
  82. }
  83. prev := map[string]any{"concurrency": a.Concurrency}
  84. if (isOpenAIOAuthLike(a) || isAnthropicOAuthLike(a)) && !a.IsShadow() && !a.IsRandomProxy() {
  85. prev = snapshotAntiDegradeConfig(a)
  86. fingerprint, tlsProfile, _ := antiDegradeModeSettings(DefaultAntiDegradeMode)
  87. if isOpenAIOAuthLike(a) {
  88. a.Extra[codexFingerprintModeExtraKey] = string(fingerprint)
  89. a.Extra = prepareCodexFingerprintExtraForUpdate(a, a.Extra)
  90. }
  91. a.Extra["enable_tls_fingerprint"] = true
  92. a.Extra["tls_fingerprint_builtin"] = tlsProfile
  93. delete(a.Extra, "tls_fingerprint_profile_id")
  94. a.Extra[AntiDegradeMarkerExtraKey] = map[string]any{"enabled": true, "mode": string(DefaultAntiDegradeMode), "max_concurrency": cap, "prev": prev, "applied_at": time.Now().UTC().Format(time.RFC3339)}
  95. a.Extra[ProtectionScopeExtraKey] = "legacy"
  96. } else {
  97. // Generic protection is a persisted concurrency bound only. It must not
  98. // advertise Codex identity or semantic checks on unsupported protocols.
  99. a.Extra[ProtectionScopeExtraKey] = "generic_v1"
  100. a.Extra[AntiDegradeMarkerExtraKey] = map[string]any{"enabled": true, "mode": "generic", "max_concurrency": cap, "prev": prev, "applied_at": time.Now().UTC().Format(time.RFC3339)}
  101. }
  102. a.Concurrency = cap
  103. }
  104. // ProtectionManagedWrite is true only for a server-internal context created by
  105. // a dedicated protection operation. No request JSON can supply this value.
  106. func ProtectionManagedWrite(ctx context.Context) bool {
  107. return ctx.Value(mode1ManagedWriteKey{}) == true
  108. }
  109. func ProtectionManagedKeys(a *Account) []string {
  110. keys := []string{AntiDegradationExtraKey, ProtectionScopeExtraKey, AntiDegradeMarkerExtraKey}
  111. // Both the v3 mode-1 policy and the explicitly selected sub2初代 policy
  112. // own the fingerprint/TLS fields they write. Preserve those fields when
  113. // an ordinary account edit submits a stale/partial extra object; otherwise
  114. // saving the modal could silently turn the legacy strategy off.
  115. if isMode1ProtectionRequested(a) || (antiDegradeEnabled(a) && antiDegradeStrategyProfile(antiDegradeMode(a)).ApplySupported) {
  116. keys = append(keys, mode1ManagedExtraKeys[1:]...)
  117. }
  118. return keys
  119. }
  120. // PreserveAccountProtection is also invoked under the repository row lock, so
  121. // a stale form or refresh cannot undo a concurrently confirmed protection change.
  122. func PreserveAccountProtection(ctx context.Context, current *Account, incoming map[string]any) map[string]any {
  123. if ProtectionManagedWrite(ctx) {
  124. return incoming
  125. }
  126. result := maps.Clone(incoming)
  127. if result == nil {
  128. result = map[string]any{}
  129. }
  130. for _, key := range ProtectionManagedKeys(current) {
  131. if value, exists := current.Extra[key]; exists {
  132. result[key] = value
  133. } else {
  134. delete(result, key)
  135. }
  136. }
  137. // Full-object imports and stale refreshes may omit the identity seed. Keep
  138. // the committed seed; unlike policy snapshots it never belongs to a caller.
  139. if seed, ok := codexFingerprintSeed(current.Extra); ok {
  140. result[codexFingerprintSeedExtraKey] = seed
  141. }
  142. return result
  143. }
  144. func BoundAccountProtectionConcurrency(a *Account) {
  145. if a == nil || !a.AntiDegradationEnabled() {
  146. return
  147. }
  148. if a.Concurrency <= 0 {
  149. a.Concurrency = AntiDegradeConcurrencyCap
  150. }
  151. // The account field is the administrator's editable ceiling. The marker
  152. // mirrors it for historical displays; strategy presets never clamp it.
  153. if marker := mode1Marker(a); marker != nil {
  154. a.Extra = maps.Clone(a.Extra)
  155. copy := maps.Clone(marker)
  156. copy["max_concurrency"] = a.Concurrency
  157. a.Extra[AntiDegradeMarkerExtraKey] = copy
  158. }
  159. }
  160. func ValidateAccountProtectionConfiguration(a *Account) error {
  161. if a != nil {
  162. if _, err := AccountTrafficPlanFor(a); err != nil {
  163. return err
  164. }
  165. }
  166. if a != nil {
  167. if err := validateRequestIntegrityExtra(a.Extra); err != nil {
  168. return err
  169. }
  170. }
  171. if !isMode1ProtectionRequested(a) {
  172. return validateRegisteredProtection(a)
  173. }
  174. if !isMode1ProtectionEnabled(a) {
  175. return infraerrors.BadRequest("MODE1_CONFIGURATION_INVALID", "账号保护配置不完整,请通过专用保护入口修复")
  176. }
  177. if issues := mode1ConfigurationIssues(a); len(issues) > 0 {
  178. return infraerrors.BadRequest("MODE1_CONFIGURATION_INVALID", issues[0])
  179. }
  180. return nil
  181. }
  182. // SetProtection enables the available policy or explicitly disables protection.
  183. // Caller must independently authenticate an administrator. Confirmation is
  184. // checked here as well as at the HTTP boundary.
  185. func (s *AntiDegradeService) SetProtection(ctx context.Context, id int64, enabled, confirmDisable bool) (*Account, error) {
  186. return s.transition(ctx, id, func(planner *AntiDegradeService) error {
  187. _, err := planner.setProtection(ctx, id, enabled, confirmDisable)
  188. return err
  189. })
  190. }
  191. func (s *AntiDegradeService) setProtection(ctx context.Context, id int64, enabled, confirmDisable bool) (*Account, error) {
  192. if !enabled && !confirmDisable {
  193. return nil, infraerrors.BadRequest("PROTECTION_CONFIRM_REQUIRED", "关闭防降智模式需要管理员明确确认")
  194. }
  195. a, err := s.admin.GetAccount(ctx, id)
  196. if err != nil {
  197. return nil, err
  198. }
  199. if a.AntiDegradationEnabled() == enabled {
  200. return a, nil
  201. }
  202. if !enabled {
  203. return s.revertAntiDegrade(ctx, id)
  204. }
  205. configureAccountProtection(a)
  206. return s.admin.UpdateAccount(context.WithValue(ctx, mode1ManagedWriteKey{}, true), id, &UpdateAccountInput{Extra: a.Extra, Concurrency: &a.Concurrency})
  207. }