account_anti_degrade.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "maps"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/config"
  8. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  9. )
  10. var ErrUnknownAntiDegradeMode = infraerrors.BadRequest("UNKNOWN_PROTECTION_STRATEGY", "未知的账号保护策略")
  11. // Account protection configures stable identity, isolated conversations,
  12. // bounded concurrency and protocol-aware request integrity. Mode 1 v3 uses
  13. // the original standard transport. These settings cannot prove model quality.
  14. // Preview is read-only; Apply preserves a snapshot for explicit Revert.
  15. const (
  16. // AntiDegradeMarkerExtraKey 一键防降智标记与快照。
  17. AntiDegradeMarkerExtraKey = "anti_degrade"
  18. // AntiDegradeConcurrencyCap 无限制并发一键收敛的目标值。
  19. AntiDegradeConcurrencyCap = 16
  20. )
  21. // AntiDegradeMode selects the current compatibility policy or the legacy policy.
  22. type AntiDegradeMode string
  23. const (
  24. AntiDegradeMode1 AntiDegradeMode = "mode1"
  25. AntiDegradeMode2 AntiDegradeMode = "mode2"
  26. AntiDegradeModeNative AntiDegradeMode = "native_baseline"
  27. AntiDegradeModeMinimal AntiDegradeMode = "minimal_compat"
  28. AntiDegradeModeSession AntiDegradeMode = "session_standard"
  29. // AntiDegradeModeLegacy is the original (sub2初代) strategy: session
  30. // fingerprint convergence with the nodejs24 TLS template. It is kept as
  31. // an explicit option so operators can reproduce the known-good baseline.
  32. AntiDegradeModeLegacy AntiDegradeMode = "legacy"
  33. AntiDegradeModeTLSNode24 AntiDegradeMode = "tls_node24"
  34. AntiDegradeModeLowConcurrency AntiDegradeMode = "low_concurrency"
  35. // DefaultAntiDegradeMode applies only when selecting a new policy. Existing
  36. // persisted policies must keep their original interpretation.
  37. DefaultAntiDegradeMode AntiDegradeMode = AntiDegradeModeLegacy
  38. )
  39. func normalizeAntiDegradeMode(mode AntiDegradeMode) AntiDegradeMode {
  40. if mode == "" {
  41. return DefaultAntiDegradeMode
  42. }
  43. return mode
  44. }
  45. func antiDegradeModeSettings(mode AntiDegradeMode) (codexFingerprintMode, string, int) {
  46. profile := antiDegradeStrategyProfile(normalizeAntiDegradeMode(mode))
  47. if profile.ID == AntiDegradeModeNative {
  48. return codexFingerprintOff, "", 0
  49. }
  50. fingerprint := codexFingerprintMode(profile.IdentityMode)
  51. if fingerprint == "" {
  52. fingerprint = codexFingerprintOff
  53. }
  54. return fingerprint, profile.TLSProfile, profile.MaxConcurrency
  55. }
  56. // AntiDegradeChange 单项改动(展示用)。
  57. type AntiDegradeChange struct {
  58. Key string `json:"key"`
  59. From any `json:"from,omitempty"`
  60. To any `json:"to"`
  61. Note string `json:"note,omitempty"`
  62. }
  63. // AntiDegradePreview 预览结果。
  64. type AntiDegradePreview struct {
  65. Runtime *ProtectionRuntimeState `json:"runtime,omitempty"`
  66. ActiveMode string `json:"active_mode"`
  67. PolicyVersion int `json:"policy_version"`
  68. IdentityReady bool `json:"identity_ready"`
  69. TLSProfile string `json:"tls_profile"`
  70. Issues []string `json:"issues"`
  71. AccountID int64 `json:"account_id"`
  72. Enabled bool `json:"enabled"`
  73. Eligible bool `json:"eligible"`
  74. Reason string `json:"reason,omitempty"`
  75. Changes []AntiDegradeChange `json:"changes"`
  76. }
  77. // AntiDegradeStore 防降智需要的账号读写接口(AdminService 已实现,无需改接口)。
  78. type AntiDegradeStore interface {
  79. GetAccount(ctx context.Context, id int64) (*Account, error)
  80. UpdateAccount(ctx context.Context, id int64, input *UpdateAccountInput) (*Account, error)
  81. }
  82. // AntiDegradeService 一键防降智服务(无状态,可直接构造)。
  83. type AntiDegradeService struct {
  84. admin AntiDegradeStore
  85. cfg *config.Config
  86. pluginManager *PluginManager
  87. }
  88. // NewAntiDegradeService 创建防降智服务。
  89. func NewAntiDegradeService(admin AntiDegradeStore) *AntiDegradeService {
  90. return &AntiDegradeService{admin: admin}
  91. }
  92. func isOpenAIOAuthLike(a *Account) bool {
  93. return a != nil && a.Platform == PlatformOpenAI &&
  94. (a.Type == AccountTypeOAuth || a.Type == AccountTypeSetupToken)
  95. }
  96. func isAnthropicOAuthLike(a *Account) bool {
  97. return a != nil && a.Platform == PlatformAnthropic &&
  98. (a.Type == AccountTypeOAuth || a.Type == AccountTypeSetupToken)
  99. }
  100. // antiDegradeEnabled 是否已启用一键防降智。
  101. func antiDegradeEnabled(a *Account) bool {
  102. if a == nil || len(a.Extra) == 0 {
  103. return false
  104. }
  105. marker, ok := a.Extra[AntiDegradeMarkerExtraKey].(map[string]any)
  106. if !ok {
  107. return false
  108. }
  109. enabled, _ := marker["enabled"].(bool)
  110. return enabled
  111. }
  112. func antiDegradePrev(a *Account) map[string]any {
  113. if a == nil || len(a.Extra) == 0 {
  114. return nil
  115. }
  116. marker, ok := a.Extra[AntiDegradeMarkerExtraKey].(map[string]any)
  117. if !ok {
  118. return nil
  119. }
  120. prev, _ := marker["prev"].(map[string]any)
  121. return prev
  122. }
  123. func antiDegradeMode(a *Account) AntiDegradeMode {
  124. if a != nil && a.Extra != nil {
  125. if marker, ok := a.Extra[AntiDegradeMarkerExtraKey].(map[string]any); ok {
  126. if raw, ok := marker["mode"].(string); ok && raw != "" {
  127. return AntiDegradeMode(raw)
  128. }
  129. }
  130. }
  131. return AntiDegradeMode1
  132. }
  133. // PreviewAntiDegrade 计算改动集(不写库)。
  134. func PreviewAntiDegrade(account *Account) AntiDegradePreview {
  135. return PreviewAntiDegradeMode(account, DefaultAntiDegradeMode)
  136. }
  137. // PreviewAntiDegradeMode 计算指定方案的真实改动集(不写库)。
  138. func PreviewAntiDegradeMode(account *Account, mode AntiDegradeMode) AntiDegradePreview {
  139. mode = normalizeAntiDegradeMode(mode)
  140. if antiDegradeStrategyProfile(mode).ID == "" {
  141. return AntiDegradePreview{Changes: []AntiDegradeChange{}, Reason: "未知的账号保护策略"}
  142. }
  143. if mode == AntiDegradeModeNative {
  144. out := AntiDegradePreview{Changes: []AntiDegradeChange{}, TLSProfile: "account"}
  145. if account == nil {
  146. out.Reason = "account not found"
  147. return out
  148. }
  149. out.AccountID = account.ID
  150. out.Enabled = antiDegradeEnabled(account)
  151. if out.Enabled {
  152. out.ActiveMode = string(antiDegradeMode(account))
  153. out.Changes = append(out.Changes, AntiDegradeChange{Key: "strategy", From: string(antiDegradeMode(account)), To: string(AntiDegradeModeNative), Note: "切换为原生基线;需要管理员确认关闭保护"})
  154. out.Eligible = true
  155. out.Reason = "原生基线不会写入新的保护配置"
  156. } else {
  157. out.Reason = "当前已是原生基线"
  158. }
  159. return out
  160. }
  161. if mode == AntiDegradeMode1 {
  162. return previewMode1(account)
  163. }
  164. out := AntiDegradePreview{Changes: []AntiDegradeChange{}}
  165. if account == nil {
  166. out.Reason = "account not found"
  167. return out
  168. }
  169. out.AccountID = account.ID
  170. out.Enabled = antiDegradeEnabled(account)
  171. if reason := antiDegradeEligibilityIssue(account, mode); reason != "" {
  172. out.Reason = reason
  173. return out
  174. }
  175. if out.Enabled {
  176. out.ActiveMode = string(antiDegradeMode(account))
  177. }
  178. // Surface the effective baseline values in previews as well as the change
  179. // list. This lets the admin UI show which strategy is actually active
  180. // instead of falling back to an "unverified" badge after a modal refresh.
  181. if _, ok := codexFingerprintSeed(account.Extra); ok {
  182. out.IdentityReady = true
  183. }
  184. effectiveMode := mode
  185. if out.Enabled {
  186. effectiveMode = antiDegradeMode(account)
  187. }
  188. _, effectiveTLS, _ := antiDegradeModeSettings(effectiveMode)
  189. if (isOpenAIOAuthLike(account) || isAnthropicOAuthLike(account)) && effectiveTLS != "" {
  190. out.TLSProfile = effectiveTLS
  191. }
  192. if out.Enabled {
  193. if antiDegradeMode(account) == mode {
  194. out.Reason = "already enabled, can revert"
  195. return out
  196. }
  197. out.Eligible = true
  198. out.Reason = "可切换到所选防降智策略"
  199. out.Changes = append(out.Changes, AntiDegradeChange{Key: "strategy", From: string(antiDegradeMode(account)), To: string(mode), Note: "先安全还原当前快照,再应用所选策略"})
  200. return out
  201. }
  202. targetFingerprint, targetTLS, targetConcurrency := antiDegradeModeSettings(mode)
  203. if isOpenAIOAuthLike(account) {
  204. if account.GetCodexFingerprintMode() != targetFingerprint {
  205. out.Changes = append(out.Changes, AntiDegradeChange{
  206. Key: "extra.codex_fingerprint_mode",
  207. From: string(account.GetCodexFingerprintMode()),
  208. To: string(targetFingerprint),
  209. Note: "账号级 Codex 身份收敛",
  210. })
  211. }
  212. if targetTLS == "standard" {
  213. if account.IsTLSFingerprintEnabled() || account.Extra["tls_fingerprint_builtin"] != nil || account.Extra["tls_fingerprint_profile_id"] != nil {
  214. out.Changes = append(out.Changes, AntiDegradeChange{Key: "transport", From: account.Extra["tls_fingerprint_builtin"], To: targetTLS, Note: "恢复标准 TLS 传输"})
  215. }
  216. } else if !account.IsTLSFingerprintEnabled() || account.Extra["tls_fingerprint_builtin"] != targetTLS {
  217. out.Changes = append(out.Changes, AntiDegradeChange{Key: "extra.tls_fingerprint_builtin", From: account.Extra["tls_fingerprint_builtin"], To: targetTLS, Note: "完整 TLS ClientHello 模板"})
  218. }
  219. } else if isAnthropicOAuthLike(account) {
  220. if targetTLS != "standard" && (!account.IsTLSFingerprintEnabled() || account.Extra["tls_fingerprint_builtin"] != targetTLS) {
  221. out.Changes = append(out.Changes, AntiDegradeChange{Key: "extra.tls_fingerprint_builtin", From: account.Extra["tls_fingerprint_builtin"], To: targetTLS, Note: "完整 TLS ClientHello 模板"})
  222. }
  223. } else {
  224. out.Reason = "platform has no fingerprint convergence; only generic items apply"
  225. }
  226. if account.Concurrency <= 0 {
  227. out.Changes = append(out.Changes, AntiDegradeChange{
  228. Key: "concurrency",
  229. From: account.Concurrency,
  230. To: targetConcurrency,
  231. Note: "不限流并发收敛,避免突发触发上游限流",
  232. })
  233. }
  234. out.Eligible = len(out.Changes) > 0
  235. // Selecting a strategy is itself a meaningful change even when the
  236. // account's low-level fields already happen to match the target values.
  237. // Persisting the marker makes the effective mode explicit and allows the
  238. // runtime/UI to distinguish legacy/mode2 from an unconfigured account.
  239. if !out.Eligible && !out.Enabled {
  240. out.Eligible = true
  241. out.Changes = append(out.Changes, AntiDegradeChange{
  242. Key: "strategy", From: string(antiDegradeMode(account)), To: string(mode),
  243. Note: "记录所选防降智策略",
  244. })
  245. }
  246. if !out.Eligible && out.Reason == "" {
  247. out.Reason = "nothing to change"
  248. }
  249. return out
  250. }
  251. // Preview 获取账号并计算改动预览(不写库,供 handler 用)。
  252. func (s *AntiDegradeService) Preview(ctx context.Context, id int64) (AntiDegradePreview, error) {
  253. return s.PreviewMode(ctx, id, DefaultAntiDegradeMode)
  254. }
  255. func (s *AntiDegradeService) PreviewMode(ctx context.Context, id int64, mode AntiDegradeMode) (AntiDegradePreview, error) {
  256. if s == nil || s.admin == nil {
  257. return AntiDegradePreview{}, errors.New("anti-degrade service unavailable")
  258. }
  259. mode = normalizeAntiDegradeMode(mode)
  260. if antiDegradeStrategyProfile(mode).ID == "" {
  261. return AntiDegradePreview{}, ErrUnknownAntiDegradeMode
  262. }
  263. account, err := s.admin.GetAccount(ctx, id)
  264. if err != nil {
  265. return AntiDegradePreview{}, err
  266. }
  267. preview := PreviewAntiDegradeMode(account, mode)
  268. preview.Runtime = ResolveProtectionRuntime(account, s.cfg, s.pluginManager)
  269. return preview, nil
  270. }
  271. // Apply handler 适配:应用一键防降智。
  272. func (s *AntiDegradeService) Apply(ctx context.Context, id int64) (*Account, error) {
  273. return s.ApplyAntiDegradeMode(ctx, id, DefaultAntiDegradeMode)
  274. }
  275. // Revert handler 适配:一键还原。
  276. func (s *AntiDegradeService) Revert(ctx context.Context, id int64) (*Account, error) {
  277. return s.RevertAntiDegrade(ctx, id)
  278. }
  279. // ApplyAntiDegrade 应用一键防降智并快照旧值。
  280. func (s *AntiDegradeService) ApplyAntiDegrade(ctx context.Context, id int64) (*Account, error) {
  281. return s.ApplyAntiDegradeMode(ctx, id, DefaultAntiDegradeMode)
  282. }
  283. // ApplyAntiDegradeMode 应用指定方案并快照旧值。
  284. func (s *AntiDegradeService) ApplyAntiDegradeMode(ctx context.Context, id int64, mode AntiDegradeMode) (*Account, error) {
  285. return s.transition(ctx, id, func(planner *AntiDegradeService) error {
  286. _, err := planner.applyAntiDegradeMode(ctx, id, mode)
  287. return err
  288. })
  289. }
  290. func (s *AntiDegradeService) applyAntiDegradeMode(ctx context.Context, id int64, mode AntiDegradeMode) (*Account, error) {
  291. if s == nil || s.admin == nil {
  292. return nil, errors.New("anti-degrade service unavailable")
  293. }
  294. mode = normalizeAntiDegradeMode(mode)
  295. if antiDegradeStrategyProfile(mode).ID == "" {
  296. return nil, ErrUnknownAntiDegradeMode
  297. }
  298. account, err := s.admin.GetAccount(ctx, id)
  299. if err != nil {
  300. return nil, err
  301. }
  302. if mode == AntiDegradeModeNative {
  303. return nil, errors.New("native baseline requires the explicit protection revert confirmation")
  304. }
  305. if reason := antiDegradeEligibilityIssue(account, mode); reason != "" {
  306. return nil, infraerrors.BadRequest("PROTECTION_NOT_ELIGIBLE", reason)
  307. }
  308. // This service operates on the transition's in-memory draft. Only its
  309. // final state is committed by transition().
  310. if antiDegradeEnabled(account) && antiDegradeMode(account) != mode {
  311. if _, err := s.revertAntiDegrade(ctx, id); err != nil {
  312. return nil, err
  313. }
  314. account, err = s.admin.GetAccount(ctx, id)
  315. if err != nil {
  316. return nil, err
  317. }
  318. }
  319. if mode == AntiDegradeMode1 {
  320. return s.applyMode1(ctx, account)
  321. }
  322. preview := PreviewAntiDegradeMode(account, mode)
  323. if preview.Enabled {
  324. return account, nil
  325. }
  326. if !preview.Eligible {
  327. return account, nil
  328. }
  329. extra := maps.Clone(account.Extra)
  330. if extra == nil {
  331. extra = make(map[string]any)
  332. }
  333. prev := snapshotAntiDegradeConfig(account)
  334. input := &UpdateAccountInput{Extra: extra}
  335. targetFingerprint, targetTLS, targetConcurrency := antiDegradeModeSettings(mode)
  336. if isOpenAIOAuthLike(account) && account.GetCodexFingerprintMode() != targetFingerprint {
  337. extra[codexFingerprintModeExtraKey] = string(targetFingerprint)
  338. // seed 由 prepareCodexFingerprintExtraForUpdate 在 Update 内自动签发。
  339. }
  340. if (isOpenAIOAuthLike(account) || isAnthropicOAuthLike(account)) && targetTLS == "standard" && (account.IsTLSFingerprintEnabled() || account.Extra["tls_fingerprint_builtin"] != nil || account.Extra["tls_fingerprint_profile_id"] != nil) {
  341. extra["enable_tls_fingerprint"] = false
  342. delete(extra, "tls_fingerprint_builtin")
  343. delete(extra, "tls_fingerprint_profile_id")
  344. } else if (isOpenAIOAuthLike(account) || isAnthropicOAuthLike(account)) && targetTLS != "standard" && (!account.IsTLSFingerprintEnabled() || account.Extra["tls_fingerprint_builtin"] != targetTLS) {
  345. extra["enable_tls_fingerprint"] = true
  346. extra["tls_fingerprint_builtin"] = targetTLS
  347. delete(extra, "tls_fingerprint_profile_id")
  348. }
  349. if account.Concurrency <= 0 {
  350. cap := targetConcurrency
  351. input.Concurrency = &cap
  352. }
  353. if account.Concurrency > 0 {
  354. targetConcurrency = account.Concurrency
  355. }
  356. extra[AntiDegradeMarkerExtraKey] = map[string]any{
  357. "enabled": true,
  358. "mode": string(mode),
  359. // Keep an explicit runtime cap in the marker for the legacy policy too;
  360. // BoundAccountProtectionConcurrency uses this value on every request.
  361. "max_concurrency": targetConcurrency,
  362. "applied_at": time.Now().UTC().Format(time.RFC3339),
  363. "prev": prev,
  364. }
  365. extra[AntiDegradationExtraKey] = true
  366. extra[ProtectionScopeExtraKey] = "legacy"
  367. return s.admin.UpdateAccount(context.WithValue(ctx, mode1ManagedWriteKey{}, true), id, input)
  368. }
  369. // RevertAntiDegrade 一键还原:恢复快照旧值并清除标记。
  370. func (s *AntiDegradeService) RevertAntiDegrade(ctx context.Context, id int64) (*Account, error) {
  371. return s.transition(ctx, id, func(planner *AntiDegradeService) error {
  372. _, err := planner.revertAntiDegrade(ctx, id)
  373. return err
  374. })
  375. }
  376. func (s *AntiDegradeService) revertAntiDegrade(ctx context.Context, id int64) (*Account, error) {
  377. if s == nil || s.admin == nil {
  378. return nil, errors.New("anti-degrade service unavailable")
  379. }
  380. account, err := s.admin.GetAccount(ctx, id)
  381. if err != nil {
  382. return nil, err
  383. }
  384. if !account.AntiDegradationEnabled() {
  385. return account, nil
  386. }
  387. prev := antiDegradePrev(account)
  388. if isMode1ProtectionRequested(account) && prev == nil {
  389. return nil, errors.New("mode1 original configuration snapshot is missing")
  390. }
  391. // 先校验快照类型:未知类型直接报错,不写库(避免半还原后快照丢失无法重试)。
  392. var concurrency *int
  393. if conc, ok := prev["concurrency"]; ok {
  394. switch v := conc.(type) {
  395. case float64:
  396. c := int(v)
  397. if v != float64(c) {
  398. return nil, errors.New("anti-degrade snapshot has invalid concurrency value")
  399. }
  400. concurrency = &c
  401. case int:
  402. concurrency = &v
  403. case int64:
  404. c := int(v)
  405. concurrency = &c
  406. default:
  407. return nil, errors.New("anti-degrade snapshot has unsupported concurrency type")
  408. }
  409. }
  410. extra := maps.Clone(account.Extra)
  411. if extra == nil {
  412. extra = make(map[string]any)
  413. }
  414. // Concurrency is now independent from identity/TLS. Restoring a strategy
  415. // must not undo the administrator's later concurrency edits.
  416. _ = concurrency // historical snapshot remains validated but is not reapplied
  417. input := &UpdateAccountInput{Extra: extra}
  418. extra[AntiDegradationExtraKey] = false
  419. extra[ProtectionScopeExtraKey] = "disabled"
  420. if isMode1ProtectionRequested(account) {
  421. for _, key := range mode1ManagedExtraKeys {
  422. if key == AntiDegradeMarkerExtraKey {
  423. continue
  424. }
  425. if value, exists := prev[key]; exists && value != nil {
  426. extra[key] = value
  427. } else {
  428. delete(extra, key)
  429. }
  430. }
  431. delete(extra, AntiDegradeMarkerExtraKey)
  432. return s.admin.UpdateAccount(context.WithValue(ctx, mode1ManagedWriteKey{}, true), id, input)
  433. }
  434. // New snapshots include all managed keys, with nil recording absence.
  435. // Historical snapshots were sparse: omitted keys must be left intact.
  436. for _, key := range mode1ManagedExtraKeys {
  437. if key == AntiDegradeMarkerExtraKey {
  438. continue
  439. }
  440. if value, exists := prev[key]; exists {
  441. if value == nil {
  442. delete(extra, key)
  443. } else {
  444. extra[key] = value
  445. }
  446. }
  447. }
  448. delete(extra, AntiDegradeMarkerExtraKey)
  449. return s.admin.UpdateAccount(context.WithValue(ctx, mode1ManagedWriteKey{}, true), id, input)
  450. }
  451. func snapshotAntiDegradeConfig(account *Account) map[string]any {
  452. prev := map[string]any{"concurrency": account.Concurrency}
  453. for _, key := range mode1ManagedExtraKeys {
  454. if key != AntiDegradeMarkerExtraKey {
  455. prev[key] = account.Extra[key]
  456. }
  457. }
  458. return prev
  459. }