fix(leonardo): 连续 3 次鉴权失败才判死,get-session 补齐浏览器头并保护 session_data

This commit is contained in:
2026-08-09 02:17:01 +08:00
parent 6ae88725df
commit 6190f814d9
3 changed files with 133 additions and 26 deletions
+29 -5
View File
@@ -1114,6 +1114,24 @@ func (s *TokenService) ImportCustomAccount(ctx context.Context, baseURL, apiKey,
return item, nil
}
// leonardoAuthStrikeLimit 是 leonardo 号被判死前允许的连续鉴权失败次数。上游偶发
// 返回 200 null / 401(cookie 轮换竞态、人机校验)时一次就判死会误杀健康号,所以要
// 连续失败到这个次数才判死;任何一次成功都会清零。
const leonardoAuthStrikeLimit = 3
// leonardoAuthStrike 记一次鉴权失败:返回要写回的 meta、当前连续失败次数,以及是否
// 该判死。
func leonardoAuthStrike(item *model.TokenAccount, reason string) (datatypes.JSONMap, int, bool) {
meta := cloneJSONMap(item.Meta)
strikes := 1
if n, ok := jsonMapInt(item.Meta, "auth_fails"); ok {
strikes = n + 1
}
meta["auth_fails"] = strikes
meta["last_auth_error"] = reason
return meta, strikes, strikes >= leonardoAuthStrikeLimit
}
// finishPending writes the terminal status/dead flag and clears the pending_check
// marker (merging any cached quota) for a background import probe.
func (s *TokenService) finishPending(ctx context.Context, pool, id, status string, dead bool, quotaMeta map[string]any) {
@@ -1469,17 +1487,23 @@ func (s *TokenService) Quota(ctx context.Context, pool, id string) (map[string]a
s.persistLeonardoCookie(ctx, item.ID, item.Value)
if err != nil {
if errors.Is(err, leonardo.ErrAuth) {
_, _ = s.tokens.Update(ctx, item.Pool, item.ID, map[string]any{
"status": "disabled",
"dead": true,
"fails": gorm.Expr("fails + 1"),
})
meta, strikes, kill := leonardoAuthStrike(item, "quota refresh: "+err.Error())
patch := map[string]any{"meta": meta, "fails": gorm.Expr("fails + 1")}
if kill {
patch["status"] = "disabled"
patch["dead"] = true
log.Printf("account leonardo/%s disabled after %d consecutive auth failures: %v", item.ID, strikes, err)
} else {
log.Printf("leonardo %s: auth failure %d/%d on quota refresh (%v) — kept active", item.ID, strikes, leonardoAuthStrikeLimit, err)
}
_, _ = s.tokens.Update(ctx, item.Pool, item.ID, patch)
}
return nil, err
}
patch := map[string]any{}
meta := cloneJSONMap(item.Meta)
meta["cached_quota_at"] = int(time.Now().Unix())
meta["auth_fails"] = 0 // cookie 还能换 token,连续失败计数清零
if remaining, ok := data["remaining"].(int); ok {
meta["cached_quota_remaining"] = remaining
// Below the per-generation floor → sink to "限额" so it stops being
+41 -1
View File
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"io"
"log"
"net/http"
"path/filepath"
"sort"
@@ -3762,9 +3763,30 @@ func (s *V1Service) markTokenFailure(ctx context.Context, pool string, token mod
// grok is intentionally excluded: a grok sso can momentarily 401 while
// still valid (upstream blip / proxy / anti-bot), so an auth failure just
// fails over for this request without permanently killing the account.
if pool == "chatgpt" || pool == "runway" || pool == "leonardo" || pool == "krea" || pool == "imagine" {
disable := pool == "chatgpt" || pool == "runway" || pool == "leonardo" || pool == "krea" || pool == "imagine"
if disable && pool == "leonardo" {
// 两道保险:先重新 get-session 复核(单次失败常是 bearer 轮换竞态),复核
// 也不过就只记一次连续失败,连续到上限才判死。
if s.leonardoCookieAlive(ctx, token) {
log.Printf("leonardo %s: auth failure on %s but cookie still authenticates — kept active", token.ID, kind)
disable = false
} else {
meta, strikes, kill := leonardoAuthStrike(&token, "auth failure on "+kind)
patch["meta"] = meta
disable = kill
if kill {
log.Printf("account leonardo/%s disabled after %d consecutive auth failures: %s", token.ID, strikes, kind)
} else {
log.Printf("leonardo %s: auth failure %d/%d on %s — kept active", token.ID, strikes, leonardoAuthStrikeLimit, kind)
}
}
}
if disable {
patch["status"] = "disabled"
patch["dead"] = true
if pool != "leonardo" {
log.Printf("account %s/%s disabled: auth failure on %s", pool, token.ID, kind)
}
}
default:
// Neither pool is auto-disabled on generic (non-auth / non-quota) failures
@@ -3775,6 +3797,24 @@ func (s *V1Service) markTokenFailure(ctx context.Context, pool string, token mod
_, _ = s.tokens.Update(ctx, pool, token.ID, patch)
}
// leonardoCookieAlive re-checks a Leonardo cookie after an auth failure by
// force-minting a session (bypassing the cached bearer). Only a cookie that
// still fails to authenticate counts as dead; a temporary upstream answer
// (403/429 人机校验) also keeps the account alive.
func (s *V1Service) leonardoCookieAlive(ctx context.Context, token model.TokenAccount) bool {
if s.leonardo == nil || strings.TrimSpace(token.Value) == "" {
return false
}
probeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
sess, err := s.leonardo.ProbeSession(probeCtx, token.Value)
if err == nil && sess != nil && strings.TrimSpace(sess.AccessToken) != "" {
s.leonardoPersistCookie(probeCtx, token.ID, token.Value)
return true
}
return !errors.Is(err, leonardo.ErrAuth)
}
// markTokenDead disables an account and marks it dead on a fatal upstream error
// (a non-overload temporary Adobe failure that ops policy treats as account death).
func (s *V1Service) markTokenDead(ctx context.Context, pool string, token model.TokenAccount, kind string) {