mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix: exclude Antigravity from OAuth 401 temp-unschedulable and escalation logic
PR #723 changed all OAuth 401 to use SetTempUnschedulable instead of SetError, and PR #761 added 401 escalation logic in tryTempUnschedulable. Both caused non-OpenAI accounts to frequently become temporarily unschedulable. - HandleUpstreamError case 401: limit temp-unschedulable to OpenAI OAuth only, other platforms (Anthropic/Gemini/Antigravity) revert to original SetError - tryTempUnschedulable: skip 401 escalation for Antigravity, let its applyErrorPolicy temp_unschedulable_rules handle 401 naturally Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -114,7 +114,7 @@ func TestCheckErrorPolicy(t *testing.T) {
|
||||
account: &Account{
|
||||
ID: 15,
|
||||
Type: AccountTypeOAuth,
|
||||
Platform: PlatformAntigravity,
|
||||
Platform: PlatformGemini, // 非 Antigravity 平台才有 401 升级逻辑
|
||||
TempUnschedulableReason: `{"status_code":401,"until_unix":1735689600}`,
|
||||
Credentials: map[string]any{
|
||||
"temp_unschedulable_enabled": true,
|
||||
@@ -131,6 +131,28 @@ func TestCheckErrorPolicy(t *testing.T) {
|
||||
body: []byte(`unauthorized`),
|
||||
expected: ErrorPolicyNone,
|
||||
},
|
||||
{
|
||||
name: "temp_unschedulable_401_antigravity_no_escalation",
|
||||
account: &Account{
|
||||
ID: 16,
|
||||
Type: AccountTypeOAuth,
|
||||
Platform: PlatformAntigravity, // Antigravity 跳过 401 升级,由 rules 正常处理
|
||||
TempUnschedulableReason: `{"status_code":401,"until_unix":1735689600}`,
|
||||
Credentials: map[string]any{
|
||||
"temp_unschedulable_enabled": true,
|
||||
"temp_unschedulable_rules": []any{
|
||||
map[string]any{
|
||||
"error_code": float64(401),
|
||||
"keywords": []any{"unauthorized"},
|
||||
"duration_minutes": float64(10),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
statusCode: 401,
|
||||
body: []byte(`unauthorized`),
|
||||
expected: ErrorPolicyTempUnscheduled, // Antigravity 不升级,继续走规则匹配
|
||||
},
|
||||
{
|
||||
name: "temp_unschedulable_body_miss_returns_none",
|
||||
account: &Account{
|
||||
|
||||
@@ -128,8 +128,9 @@ func (s *RateLimitService) HandleUpstreamError(ctx context.Context, account *Acc
|
||||
}
|
||||
// 其他 400 错误(如参数问题)不处理,不禁用账号
|
||||
case 401:
|
||||
// 对所有 OAuth 账号在 401 错误时调用缓存失效并强制下次刷新
|
||||
if account.Type == AccountTypeOAuth {
|
||||
// OpenAI OAuth 账号在 401 错误时临时不可调度(保持 active 让刷新服务恢复);
|
||||
// 其他平台 OAuth 账号保持原有 SetError 行为(Antigravity 主流程不走此路径)。
|
||||
if account.Type == AccountTypeOAuth && account.Platform == PlatformOpenAI {
|
||||
// 1. 失效缓存
|
||||
if s.tokenCacheInvalidator != nil {
|
||||
if err := s.tokenCacheInvalidator.InvalidateToken(ctx, account); err != nil {
|
||||
@@ -1093,7 +1094,8 @@ func (s *RateLimitService) tryTempUnschedulable(ctx context.Context, account *Ac
|
||||
}
|
||||
// 401 首次命中可临时不可调度(给 token 刷新窗口);
|
||||
// 若历史上已因 401 进入过临时不可调度,则本次应升级为 error(返回 false 交由默认错误逻辑处理)。
|
||||
if statusCode == http.StatusUnauthorized {
|
||||
// Antigravity 跳过:其 401 由 applyErrorPolicy 的 temp_unschedulable_rules 自行控制,无需升级逻辑。
|
||||
if statusCode == http.StatusUnauthorized && account.Platform != PlatformAntigravity {
|
||||
reason := account.TempUnschedulableReason
|
||||
// 缓存可能没有 reason,从 DB 回退读取
|
||||
if reason == "" {
|
||||
|
||||
@@ -28,6 +28,7 @@ func (r *dbFallbackRepoStub) GetByID(ctx context.Context, id int64) (*Account, e
|
||||
func TestCheckErrorPolicy_401_DBFallback_Escalates(t *testing.T) {
|
||||
// Scenario: cache account has empty TempUnschedulableReason (cache miss),
|
||||
// but DB account has a previous 401 record → should escalate to ErrorPolicyNone.
|
||||
// 注意:Antigravity 跳过 401 升级,此测试使用 Gemini 平台验证升级逻辑。
|
||||
repo := &dbFallbackRepoStub{
|
||||
dbAccount: &Account{
|
||||
ID: 20,
|
||||
@@ -39,7 +40,7 @@ func TestCheckErrorPolicy_401_DBFallback_Escalates(t *testing.T) {
|
||||
account := &Account{
|
||||
ID: 20,
|
||||
Type: AccountTypeOAuth,
|
||||
Platform: PlatformAntigravity,
|
||||
Platform: PlatformGemini,
|
||||
TempUnschedulableReason: "", // cache miss — reason is empty
|
||||
Credentials: map[string]any{
|
||||
"temp_unschedulable_enabled": true,
|
||||
|
||||
@@ -41,13 +41,37 @@ func (r *tokenCacheInvalidatorRecorder) InvalidateToken(ctx context.Context, acc
|
||||
return r.err
|
||||
}
|
||||
|
||||
func TestRateLimitService_HandleUpstreamError_OAuth401SetsTempUnschedulable(t *testing.T) {
|
||||
// TestRateLimitService_HandleUpstreamError_OpenAIOAuth401SetsTempUnschedulable
|
||||
// 仅 OpenAI OAuth 账号 401 使用 temp_unschedulable
|
||||
func TestRateLimitService_HandleUpstreamError_OpenAIOAuth401SetsTempUnschedulable(t *testing.T) {
|
||||
repo := &rateLimitAccountRepoStub{}
|
||||
invalidator := &tokenCacheInvalidatorRecorder{}
|
||||
service := NewRateLimitService(repo, nil, &config.Config{}, nil, nil)
|
||||
service.SetTokenCacheInvalidator(invalidator)
|
||||
account := &Account{
|
||||
ID: 100,
|
||||
Platform: PlatformOpenAI,
|
||||
Type: AccountTypeOAuth,
|
||||
}
|
||||
|
||||
shouldDisable := service.HandleUpstreamError(context.Background(), account, 401, http.Header{}, []byte("unauthorized"))
|
||||
|
||||
require.True(t, shouldDisable)
|
||||
require.Equal(t, 0, repo.setErrorCalls)
|
||||
require.Equal(t, 1, repo.tempCalls)
|
||||
require.Len(t, invalidator.accounts, 1)
|
||||
}
|
||||
|
||||
// TestRateLimitService_HandleUpstreamError_NonOpenAIOAuth401SetsError
|
||||
// 非 OpenAI 的 OAuth 账号 401 走原有 SetError 行为
|
||||
func TestRateLimitService_HandleUpstreamError_NonOpenAIOAuth401SetsError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
platform string
|
||||
}{
|
||||
{name: "gemini", platform: PlatformGemini},
|
||||
{name: "antigravity", platform: PlatformAntigravity},
|
||||
{name: "anthropic", platform: PlatformAnthropic},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -60,29 +84,20 @@ func TestRateLimitService_HandleUpstreamError_OAuth401SetsTempUnschedulable(t *t
|
||||
ID: 100,
|
||||
Platform: tt.platform,
|
||||
Type: AccountTypeOAuth,
|
||||
Credentials: map[string]any{
|
||||
"temp_unschedulable_enabled": true,
|
||||
"temp_unschedulable_rules": []any{
|
||||
map[string]any{
|
||||
"error_code": 401,
|
||||
"keywords": []any{"unauthorized"},
|
||||
"duration_minutes": 30,
|
||||
"description": "custom rule",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
shouldDisable := service.HandleUpstreamError(context.Background(), account, 401, http.Header{}, []byte("unauthorized"))
|
||||
|
||||
require.True(t, shouldDisable)
|
||||
require.Equal(t, 0, repo.setErrorCalls)
|
||||
require.Equal(t, 1, repo.tempCalls)
|
||||
require.Len(t, invalidator.accounts, 1)
|
||||
require.Equal(t, 1, repo.setErrorCalls)
|
||||
require.Equal(t, 0, repo.tempCalls)
|
||||
require.Contains(t, repo.lastErrorMsg, "Authentication failed (401)")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRateLimitService_HandleUpstreamError_OAuth401InvalidatorError
|
||||
// OpenAI OAuth 401 缓存失效出错时仍走 temp_unschedulable
|
||||
func TestRateLimitService_HandleUpstreamError_OAuth401InvalidatorError(t *testing.T) {
|
||||
repo := &rateLimitAccountRepoStub{}
|
||||
invalidator := &tokenCacheInvalidatorRecorder{err: errors.New("boom")}
|
||||
@@ -90,7 +105,7 @@ func TestRateLimitService_HandleUpstreamError_OAuth401InvalidatorError(t *testin
|
||||
service.SetTokenCacheInvalidator(invalidator)
|
||||
account := &Account{
|
||||
ID: 101,
|
||||
Platform: PlatformGemini,
|
||||
Platform: PlatformOpenAI,
|
||||
Type: AccountTypeOAuth,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user