From 2c47344ed99a1e2ee364db3307761de713bacf00 Mon Sep 17 00:00:00 2001 From: GlossSeaDress Date: Mon, 10 Aug 2026 12:13:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(leonardo):=20cookie=20=E5=86=99=E5=9B=9E?= =?UTF-8?q?=E6=94=B9=20CAS=EF=BC=8C=E5=B9=B6=E8=A1=A5=E4=B8=8A=E9=A2=9D?= =?UTF-8?q?=E5=BA=A6=E5=AF=B9=E8=B4=A6=E5=90=8E=E7=9A=84=E8=BD=AE=E6=8D=A2?= =?UTF-8?q?=E5=86=99=E5=9B=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leonardo 只认加密的 session_data 缓存,轮换值一旦没存住账号就会被判死。写回原先是无条件 update:长任务(生成、慢额度查询)手里的旧 cookie 完成时会盖掉期间 keepalive 存好的新值。改为 SwapValue(WHERE value = 旧值 才写),旧值无法覆盖新值;reconcileLeonardoCredits 原先完全没写回 FetchCreditsBalance 带回的轮换值,补上。 --- backend/internal/repo/token_repo.go | 17 +++++++++++++++++ backend/internal/service/tokens.go | 6 +++--- backend/internal/service/v1.go | 3 ++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/backend/internal/repo/token_repo.go b/backend/internal/repo/token_repo.go index c17bceb..9b2d9e9 100644 --- a/backend/internal/repo/token_repo.go +++ b/backend/internal/repo/token_repo.go @@ -87,6 +87,23 @@ func (r *TokenRepository) Update(ctx context.Context, pool, id string, patch map return r.Get(ctx, pool, id) } +// SwapValue replaces an account's credential only while the stored one is still +// the value the caller started from. A rotating cookie is minted from whatever +// was in the row, so a goroutine that has been holding an older copy (a long +// render, a slow quota probe) must NOT be allowed to write it back over a newer +// rotation — that older copy no longer authenticates, and the account then looks +// dead. Reports whether the row was updated. +func (r *TokenRepository) SwapValue(ctx context.Context, pool, id, from, to string) (bool, error) { + res := r.db.WithContext(ctx). + Model(&model.TokenAccount{}). + Where("pool = ? AND id = ? AND value = ?", pool, id, from). + Updates(map[string]any{"value": to, "updated_at": time.Now()}) + if res.Error != nil { + return false, res.Error + } + return res.RowsAffected > 0, nil +} + // ReserveQuota atomically pre-deducts `amount` from an account's cached image // token balance under a row lock, so concurrent picks of the same near-empty // account can never over-commit it. Returns: diff --git a/backend/internal/service/tokens.go b/backend/internal/service/tokens.go index cd9b42e..eb99657 100644 --- a/backend/internal/service/tokens.go +++ b/backend/internal/service/tokens.go @@ -195,10 +195,10 @@ func (s *TokenService) RefreshLeonardoSessions(ctx context.Context) { } continue // leave the stamp alone so the next tick retries } - fields := map[string]any{} if fresh, ok := s.leonardo.RotatedCookie(a.Value); ok && strings.TrimSpace(fresh) != "" { - fields["value"] = fresh + _, _ = s.tokens.SwapValue(callCtx, "leonardo", a.ID, a.Value, fresh) } + fields := map[string]any{} meta := cloneJSONMap(a.Meta) meta[leonardoKeptAtKey] = int(time.Now().Unix()) fields["meta"] = meta @@ -459,7 +459,7 @@ func (s *TokenService) persistLeonardoCookie(ctx context.Context, tokenID, cooki return } if fresh, ok := s.leonardo.RotatedCookie(cookie); ok && strings.TrimSpace(fresh) != "" { - _, _ = s.tokens.Update(ctx, "leonardo", tokenID, map[string]any{"value": fresh}) + _, _ = s.tokens.SwapValue(ctx, "leonardo", tokenID, cookie, fresh) } } diff --git a/backend/internal/service/v1.go b/backend/internal/service/v1.go index c6afe47..076593a 100644 --- a/backend/internal/service/v1.go +++ b/backend/internal/service/v1.go @@ -3046,7 +3046,7 @@ func (s *V1Service) leonardoPersistCookie(ctx context.Context, tokenID, cookie s if !ok || strings.TrimSpace(fresh) == "" { return cookie } - _, _ = s.tokens.Update(ctx, "leonardo", tokenID, map[string]any{"value": fresh}) + _, _ = s.tokens.SwapValue(ctx, "leonardo", tokenID, cookie, fresh) return fresh } @@ -3058,6 +3058,7 @@ func (s *V1Service) reconcileLeonardoCredits(ctx context.Context, tokenID, cooki return } data, err := s.leonardo.FetchCreditsBalance(ctx, cookie) + s.leonardoPersistCookie(ctx, tokenID, cookie) if err != nil { return }