2026-02-10 23:13:37 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"log"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2026-02-10 23:35:21 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
2026-02-10 23:13:37 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// TempUnscheduler 用于 HandleFailoverError 中同账号重试耗尽后的临时封禁。
|
|
|
|
|
|
// GatewayService 隐式实现此接口。
|
|
|
|
|
|
type TempUnscheduler interface {
|
|
|
|
|
|
TempUnscheduleRetryableError(ctx context.Context, accountID int64, failoverErr *service.UpstreamFailoverError)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// FailoverAction 表示 failover 错误处理后的下一步动作
|
|
|
|
|
|
type FailoverAction int
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
// FailoverRetry 同账号重试(调用方应 continue 重新进入循环,不更换账号)
|
|
|
|
|
|
FailoverRetry FailoverAction = iota
|
|
|
|
|
|
// FailoverSwitch 切换账号(调用方应 continue 重新选择账号)
|
|
|
|
|
|
FailoverSwitch
|
|
|
|
|
|
// FailoverExhausted 切换次数耗尽(调用方应返回错误响应)
|
|
|
|
|
|
FailoverExhausted
|
|
|
|
|
|
// FailoverCanceled context 已取消(调用方应直接 return)
|
|
|
|
|
|
FailoverCanceled
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
// maxSameAccountRetries 同账号重试次数上限(针对 RetryableOnSameAccount 错误)
|
|
|
|
|
|
maxSameAccountRetries = 2
|
|
|
|
|
|
// sameAccountRetryDelay 同账号重试间隔
|
|
|
|
|
|
sameAccountRetryDelay = 500 * time.Millisecond
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// FailoverState 跨循环迭代共享的 failover 状态
|
|
|
|
|
|
type FailoverState struct {
|
|
|
|
|
|
SwitchCount int
|
|
|
|
|
|
MaxSwitches int
|
|
|
|
|
|
FailedAccountIDs map[int64]struct{}
|
|
|
|
|
|
SameAccountRetryCount map[int64]int
|
|
|
|
|
|
LastFailoverErr *service.UpstreamFailoverError
|
|
|
|
|
|
ForceCacheBilling bool
|
|
|
|
|
|
hasBoundSession bool
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewFailoverState 创建 failover 状态
|
|
|
|
|
|
func NewFailoverState(maxSwitches int, hasBoundSession bool) *FailoverState {
|
|
|
|
|
|
return &FailoverState{
|
|
|
|
|
|
MaxSwitches: maxSwitches,
|
|
|
|
|
|
FailedAccountIDs: make(map[int64]struct{}),
|
|
|
|
|
|
SameAccountRetryCount: make(map[int64]int),
|
|
|
|
|
|
hasBoundSession: hasBoundSession,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// HandleFailoverError 处理 UpstreamFailoverError,返回下一步动作。
|
|
|
|
|
|
// 包含:缓存计费判断、同账号重试、临时封禁、切换计数、Antigravity 延时。
|
|
|
|
|
|
func (s *FailoverState) HandleFailoverError(
|
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
|
gatewayService TempUnscheduler,
|
|
|
|
|
|
accountID int64,
|
|
|
|
|
|
platform string,
|
|
|
|
|
|
failoverErr *service.UpstreamFailoverError,
|
|
|
|
|
|
) FailoverAction {
|
|
|
|
|
|
s.LastFailoverErr = failoverErr
|
|
|
|
|
|
|
|
|
|
|
|
// 缓存计费判断
|
|
|
|
|
|
if needForceCacheBilling(s.hasBoundSession, failoverErr) {
|
|
|
|
|
|
s.ForceCacheBilling = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 同账号重试:对 RetryableOnSameAccount 的临时性错误,先在同一账号上重试
|
|
|
|
|
|
if failoverErr.RetryableOnSameAccount && s.SameAccountRetryCount[accountID] < maxSameAccountRetries {
|
|
|
|
|
|
s.SameAccountRetryCount[accountID]++
|
|
|
|
|
|
log.Printf("Account %d: retryable error %d, same-account retry %d/%d",
|
|
|
|
|
|
accountID, failoverErr.StatusCode, s.SameAccountRetryCount[accountID], maxSameAccountRetries)
|
|
|
|
|
|
if !sleepWithContext(ctx, sameAccountRetryDelay) {
|
|
|
|
|
|
return FailoverCanceled
|
|
|
|
|
|
}
|
|
|
|
|
|
return FailoverRetry
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 同账号重试用尽,执行临时封禁
|
|
|
|
|
|
if failoverErr.RetryableOnSameAccount {
|
|
|
|
|
|
gatewayService.TempUnscheduleRetryableError(ctx, accountID, failoverErr)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加入失败列表
|
|
|
|
|
|
s.FailedAccountIDs[accountID] = struct{}{}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否耗尽
|
|
|
|
|
|
if s.SwitchCount >= s.MaxSwitches {
|
|
|
|
|
|
return FailoverExhausted
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 递增切换计数
|
|
|
|
|
|
s.SwitchCount++
|
|
|
|
|
|
log.Printf("Account %d: upstream error %d, switching account %d/%d",
|
|
|
|
|
|
accountID, failoverErr.StatusCode, s.SwitchCount, s.MaxSwitches)
|
|
|
|
|
|
|
|
|
|
|
|
// Antigravity 平台换号线性递增延时
|
|
|
|
|
|
if platform == service.PlatformAntigravity {
|
|
|
|
|
|
if !sleepFailoverDelay(ctx, s.SwitchCount) {
|
|
|
|
|
|
return FailoverCanceled
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return FailoverSwitch
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// sleepWithContext 等待指定时长,返回 false 表示 context 已取消。
|
|
|
|
|
|
func sleepWithContext(ctx context.Context, d time.Duration) bool {
|
|
|
|
|
|
if d <= 0 {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
select {
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
|
return false
|
|
|
|
|
|
case <-time.After(d):
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|