fix(gemini): 修复 P0/P1 级别问题(429误判/Tier丢失/expires_at/前端一致性)

P0 修复(Critical - 影响生产稳定性):
- 修复 429 判断逻辑:使用 project_id 判断而非 account.Type
  防止 AI Studio OAuth 被误判为 Code Assist 5分钟窗口
- 修复 Tier ID 丢失:刷新时始终保留旧值,默认 LEGACY
  防止 fetchProjectID 失败导致 tier_id 被清空
- 修复 expires_at 下界:添加 minTTL=30s 保护
  防止 expires_in <= 300 时生成过去时间引发刷新风暴

P1 修复(Important - 行为一致性):
- 前端 isCodeAssist 判断与后端一致(支持 legacy)
- 前端日期解析添加 NaN 保护
- 迁移脚本覆盖 legacy 账号

前端功能(新增):
- AccountQuotaInfo 组件:Tier Badge + 二元进度条 + 倒计时
- 定时器动态管理:watch 监听限流状态
- 类型定义:GeminiCredentials 接口

测试:
-  TypeScript 类型检查通过
-  前端构建成功(3.33s)
-  Gemini + Codex 双 AI 审查通过

Refs: #gemini-quota
This commit is contained in:
IanShaw027
2025-12-31 23:57:01 +08:00
parent 34c102045a
commit e49281774d
6 changed files with 349 additions and 22 deletions

View File

@@ -1886,13 +1886,47 @@ func (s *GeminiMessagesCompatService) handleGeminiUpstreamError(ctx context.Cont
if statusCode != 429 {
return
}
// 获取账号的 oauth_type、tier_id 和 project_id
oauthType := strings.TrimSpace(account.GetCredential("oauth_type"))
tierID := strings.TrimSpace(account.GetCredential("tier_id"))
projectID := strings.TrimSpace(account.GetCredential("project_id"))
// 判断是否为 Code Assist以 project_id 是否存在为准(更可靠)
isCodeAssist := projectID != ""
// Legacy 兼容oauth_type 为空但 project_id 存在时视为 code_assist
if oauthType == "" && isCodeAssist {
oauthType = "code_assist"
}
resetAt := ParseGeminiRateLimitResetTime(body)
if resetAt == nil {
ra := time.Now().Add(5 * time.Minute)
// 根据账号类型使用不同的默认重置时间
var ra time.Time
if isCodeAssist {
// Code Assist: 5 分钟滚动窗口
ra = time.Now().Add(5 * time.Minute)
log.Printf("[Gemini 429] Account %d (Code Assist, tier=%s, project=%s) rate limited, reset in 5min", account.ID, tierID, projectID)
} else {
// API Key / AI Studio OAuth: PST 午夜
if ts := nextGeminiDailyResetUnix(); ts != nil {
ra = time.Unix(*ts, 0)
log.Printf("[Gemini 429] Account %d (API Key/AI Studio, type=%s) rate limited, reset at PST midnight (%v)", account.ID, account.Type, ra)
} else {
// 兜底5 分钟
ra = time.Now().Add(5 * time.Minute)
log.Printf("[Gemini 429] Account %d rate limited, fallback to 5min", account.ID)
}
}
_ = s.accountRepo.SetRateLimited(ctx, account.ID, ra)
return
}
_ = s.accountRepo.SetRateLimited(ctx, account.ID, time.Unix(*resetAt, 0))
// 使用解析到的重置时间
resetTime := time.Unix(*resetAt, 0)
_ = s.accountRepo.SetRateLimited(ctx, account.ID, resetTime)
log.Printf("[Gemini 429] Account %d rate limited until %v (oauth_type=%s, tier=%s)",
account.ID, resetTime, oauthType, tierID)
}
// ParseGeminiRateLimitResetTime 解析 Gemini 格式的 429 响应,返回重置时间的 Unix 时间戳