diff --git a/backend/internal/repository/account_repo.go b/backend/internal/repository/account_repo.go index 212523a4fb..1d4f9d0d00 100644 --- a/backend/internal/repository/account_repo.go +++ b/backend/internal/repository/account_repo.go @@ -1107,6 +1107,7 @@ func (r *accountRepository) ListOAuthRefreshCandidatePage(ctx context.Context, o SELECT id FROM accounts WHERE deleted_at IS NULL + AND schedulable = TRUE AND platform = ANY($1) AND id > $2` if options.ActiveOnly { diff --git a/backend/internal/repository/account_repo_integration_test.go b/backend/internal/repository/account_repo_integration_test.go index fa4eed7f5c..55f4cd64db 100644 --- a/backend/internal/repository/account_repo_integration_test.go +++ b/backend/internal/repository/account_repo_integration_test.go @@ -327,6 +327,14 @@ func (s *AccountRepoSuite) TestListOAuthRefreshCandidatePage_GrokCursorAndExclus "expires_at": now.Add(30 * time.Minute).Format(time.RFC3339), }, }) + unschedulable := mustCreateAccount(s.T(), s.client, &service.Account{ + Name: "grok-oauth-unschedulable-excluded", + Platform: service.PlatformGrok, + Type: service.AccountTypeOAuth, + Status: service.StatusActive, + Credentials: map[string]any{"refresh_token": "refresh-unschedulable"}, + }) + s.Require().NoError(s.client.Account.UpdateOneID(unschedulable.ID).SetSchedulable(false).Exec(s.ctx)) mustCreateAccount(s.T(), s.client, &service.Account{ Name: "grok-api-key-excluded", Platform: service.PlatformGrok, @@ -386,6 +394,7 @@ func (s *AccountRepoSuite) TestListOAuthRefreshCandidatePage_GrokCursorAndExclus first := firstPage.Accounts s.Require().Len(first, 2) s.Require().Equal([]int64{valid1.ID, valid2.ID}, []int64{first[0].ID, first[1].ID}) + s.Require().NotContains([]int64{first[0].ID, first[1].ID}, unschedulable.ID) options.AfterID = first[len(first)-1].ID secondPage, err := s.repo.ListOAuthRefreshCandidatePage(s.ctx, options) diff --git a/backend/internal/repository/account_repo_temp_unsched_test.go b/backend/internal/repository/account_repo_temp_unsched_test.go index df1e7abf3b..b7604e6c3f 100644 --- a/backend/internal/repository/account_repo_temp_unsched_test.go +++ b/backend/internal/repository/account_repo_temp_unsched_test.go @@ -276,6 +276,8 @@ func TestAccountRepository_ListOAuthRefreshCandidatePage_SQLFilter(t *testing.T) normalized := normalizeSQLWhitespace(capturedSQL) require.Contains(t, normalized, "deleted_at IS NULL") + require.Contains(t, normalized, "schedulable = TRUE", + "permanently unschedulable accounts must not remain OAuth refresh candidates") require.Contains(t, normalized, "status = 'active'") // setup-token 的 access_token 同为 8h 短期令牌,必须与 oauth 一起纳入后台刷新候选 require.Contains(t, normalized, "type IN ('oauth', 'setup-token')") diff --git a/backend/internal/service/token_refresh_service_candidates_test.go b/backend/internal/service/token_refresh_service_candidates_test.go index ac4e3cf4f0..285caa3b91 100644 --- a/backend/internal/service/token_refresh_service_candidates_test.go +++ b/backend/internal/service/token_refresh_service_candidates_test.go @@ -51,6 +51,7 @@ func (r *tokenRefreshCandidateRepo) ListOAuthRefreshCandidatePage(_ context.Cont } } if options.ActiveOnly && account.Status != StatusActive || + !account.Schedulable || account.Type != AccountTypeOAuth || !platformAllowed || options.RequireRefreshToken && strings.TrimSpace(refreshToken) == "" || @@ -122,6 +123,7 @@ func TestTokenRefreshService_ProcessRefreshUsesOAuthRefreshCandidates(t *testing Platform: PlatformOpenAI, Type: AccountTypeOAuth, Status: StatusActive, + Schedulable: true, Credentials: map[string]any{"refresh_token": "refresh-token"}, }, { @@ -129,6 +131,7 @@ func TestTokenRefreshService_ProcessRefreshUsesOAuthRefreshCandidates(t *testing Platform: PlatformOpenAI, Type: AccountTypeOAuth, Status: StatusActive, + Schedulable: true, Credentials: map[string]any{}, }, { @@ -143,6 +146,7 @@ func TestTokenRefreshService_ProcessRefreshUsesOAuthRefreshCandidates(t *testing Platform: PlatformAntigravity, Type: AccountTypeOAuth, Status: StatusActive, + Schedulable: true, Credentials: map[string]any{"refresh_token": "refresh-token"}, TempUnschedulableUntil: &future, TempUnschedulableReason: "token refresh retry exhausted: network timeout", @@ -152,6 +156,7 @@ func TestTokenRefreshService_ProcessRefreshUsesOAuthRefreshCandidates(t *testing Platform: "other", Type: AccountTypeOAuth, Status: StatusActive, + Schedulable: true, Credentials: map[string]any{"refresh_token": "refresh-token"}, }, { @@ -159,11 +164,20 @@ func TestTokenRefreshService_ProcessRefreshUsesOAuthRefreshCandidates(t *testing Platform: PlatformAntigravity, Type: AccountTypeOAuth, Status: StatusActive, + Schedulable: true, Credentials: map[string]any{"refresh_token": "refresh-token"}, Extra: map[string]any{"privacy_mode": AntigravityPrivacySet}, TempUnschedulableUntil: &future, TempUnschedulableReason: "OAuth 401: unauthorized", }, + { + ID: 7, + Platform: PlatformOpenAI, + Type: AccountTypeOAuth, + Status: StatusActive, + Schedulable: false, + Credentials: map[string]any{"refresh_token": "permanently-rejected-token"}, + }, }, } svc := &TokenRefreshService{