2025-12-25 06:45:03 -08:00
|
|
|
package admin
|
|
|
|
|
|
|
|
|
|
import (
|
2025-12-25 21:24:53 -08:00
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2025-12-25 06:45:03 -08:00
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type GeminiOAuthHandler struct {
|
|
|
|
|
geminiOAuthService *service.GeminiOAuthService
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewGeminiOAuthHandler(geminiOAuthService *service.GeminiOAuthService) *GeminiOAuthHandler {
|
|
|
|
|
return &GeminiOAuthHandler{geminiOAuthService: geminiOAuthService}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-25 23:52:02 -08:00
|
|
|
// GET /api/v1/admin/gemini/oauth/capabilities
|
|
|
|
|
func (h *GeminiOAuthHandler) GetCapabilities(c *gin.Context) {
|
|
|
|
|
cfg := h.geminiOAuthService.GetOAuthConfig()
|
|
|
|
|
response.Success(c, cfg)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-25 06:45:03 -08:00
|
|
|
type GeminiGenerateAuthURLRequest struct {
|
2025-12-25 21:24:53 -08:00
|
|
|
ProxyID *int64 `json:"proxy_id"`
|
|
|
|
|
ProjectID string `json:"project_id"`
|
|
|
|
|
// OAuth 类型: "code_assist" (需要 project_id) 或 "ai_studio" (不需要 project_id)
|
|
|
|
|
// 默认为 "code_assist" 以保持向后兼容
|
|
|
|
|
OAuthType string `json:"oauth_type"`
|
2025-12-25 06:45:03 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GenerateAuthURL generates Google OAuth authorization URL for Gemini.
|
|
|
|
|
// POST /api/v1/admin/gemini/oauth/auth-url
|
|
|
|
|
func (h *GeminiOAuthHandler) GenerateAuthURL(c *gin.Context) {
|
|
|
|
|
var req GeminiGenerateAuthURLRequest
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-25 21:24:53 -08:00
|
|
|
// 默认使用 code_assist 以保持向后兼容
|
|
|
|
|
oauthType := strings.TrimSpace(req.OAuthType)
|
|
|
|
|
if oauthType == "" {
|
|
|
|
|
oauthType = "code_assist"
|
|
|
|
|
}
|
feat(gemini): 添加 Google One 存储空间推断 Tier 功能
## 功能概述
通过 Google Drive API 获取存储空间配额来推断 Google One 订阅等级,并优化统一的配额显示系统。
## 后端改动
- 新增 Drive API 客户端 (drive_client.go)
- 支持代理和指数退避重试
- 处理 403/429 错误
- 添加 Tier 推断逻辑 (inferGoogleOneTier)
- 支持 6 种 tier 类型:AI_PREMIUM, GOOGLE_ONE_STANDARD, GOOGLE_ONE_BASIC, FREE, GOOGLE_ONE_UNKNOWN, GOOGLE_ONE_UNLIMITED
- 集成到 OAuth 流程
- ExchangeCode: 授权时自动获取 tier
- RefreshAccountToken: Token 刷新时更新 tier (24小时缓存)
- 新增管理 API 端点
- POST /api/v1/admin/accounts/:id/refresh-tier (单个账号刷新)
- POST /api/v1/admin/accounts/batch-refresh-tier (批量刷新)
## 前端改动
- 更新 AccountQuotaInfo.vue
- 添加 Google One tier 标签映射
- 添加 tier 颜色样式 (紫色/蓝色/绿色/灰色/琥珀色)
- 更新 AccountUsageCell.vue
- 添加 Google One tier 显示逻辑
- 根据 oauth_type 区分显示方式
- 添加国际化翻译 (en.ts, zh.ts)
- aiPremium, standard, basic, free, personal, unlimited
## Tier 推断规则
- >= 2TB: AI Premium
- >= 200GB: Google One Standard
- >= 100GB: Google One Basic
- >= 15GB: Free
- > 100TB: Unlimited (G Suite legacy)
- 其他/失败: Unknown (显示为 Personal)
## 优雅降级
- Drive API 失败时使用 GOOGLE_ONE_UNKNOWN
- 不阻断 OAuth 流程
- 24小时缓存避免频繁调用
## 测试
- ✅ 后端编译成功
- ✅ 前端构建成功
- ✅ 所有代码符合现有规范
2025-12-31 21:45:24 -08:00
|
|
|
if oauthType != "code_assist" && oauthType != "google_one" && oauthType != "ai_studio" {
|
|
|
|
|
response.BadRequest(c, "Invalid oauth_type: must be 'code_assist', 'google_one', or 'ai_studio'")
|
2025-12-25 21:24:53 -08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-25 23:52:02 -08:00
|
|
|
// Always pass the "hosted" callback URI; the OAuth service may override it depending on
|
|
|
|
|
// oauth_type and whether the built-in Gemini CLI OAuth client is used.
|
2025-12-25 21:24:53 -08:00
|
|
|
redirectURI := deriveGeminiRedirectURI(c)
|
|
|
|
|
result, err := h.geminiOAuthService.GenerateAuthURL(c.Request.Context(), req.ProxyID, redirectURI, req.ProjectID, oauthType)
|
2025-12-25 06:45:03 -08:00
|
|
|
if err != nil {
|
2025-12-25 21:24:53 -08:00
|
|
|
msg := err.Error()
|
|
|
|
|
// Treat missing/invalid OAuth client configuration as a user/config error.
|
|
|
|
|
if strings.Contains(msg, "OAuth client not configured") || strings.Contains(msg, "requires your own OAuth Client") {
|
|
|
|
|
response.BadRequest(c, "Failed to generate auth URL: "+msg)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.InternalError(c, "Failed to generate auth URL: "+msg)
|
2025-12-25 06:45:03 -08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response.Success(c, result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type GeminiExchangeCodeRequest struct {
|
2025-12-25 21:24:53 -08:00
|
|
|
SessionID string `json:"session_id" binding:"required"`
|
|
|
|
|
State string `json:"state" binding:"required"`
|
|
|
|
|
Code string `json:"code" binding:"required"`
|
|
|
|
|
ProxyID *int64 `json:"proxy_id"`
|
|
|
|
|
// OAuth 类型: "code_assist" 或 "ai_studio",需要与 GenerateAuthURL 时的类型一致
|
|
|
|
|
OAuthType string `json:"oauth_type"`
|
2025-12-25 06:45:03 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExchangeCode exchanges authorization code for tokens.
|
|
|
|
|
// POST /api/v1/admin/gemini/oauth/exchange-code
|
|
|
|
|
func (h *GeminiOAuthHandler) ExchangeCode(c *gin.Context) {
|
|
|
|
|
var req GeminiExchangeCodeRequest
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-25 21:24:53 -08:00
|
|
|
// 默认使用 code_assist 以保持向后兼容
|
|
|
|
|
oauthType := strings.TrimSpace(req.OAuthType)
|
|
|
|
|
if oauthType == "" {
|
|
|
|
|
oauthType = "code_assist"
|
|
|
|
|
}
|
feat(gemini): 添加 Google One 存储空间推断 Tier 功能
## 功能概述
通过 Google Drive API 获取存储空间配额来推断 Google One 订阅等级,并优化统一的配额显示系统。
## 后端改动
- 新增 Drive API 客户端 (drive_client.go)
- 支持代理和指数退避重试
- 处理 403/429 错误
- 添加 Tier 推断逻辑 (inferGoogleOneTier)
- 支持 6 种 tier 类型:AI_PREMIUM, GOOGLE_ONE_STANDARD, GOOGLE_ONE_BASIC, FREE, GOOGLE_ONE_UNKNOWN, GOOGLE_ONE_UNLIMITED
- 集成到 OAuth 流程
- ExchangeCode: 授权时自动获取 tier
- RefreshAccountToken: Token 刷新时更新 tier (24小时缓存)
- 新增管理 API 端点
- POST /api/v1/admin/accounts/:id/refresh-tier (单个账号刷新)
- POST /api/v1/admin/accounts/batch-refresh-tier (批量刷新)
## 前端改动
- 更新 AccountQuotaInfo.vue
- 添加 Google One tier 标签映射
- 添加 tier 颜色样式 (紫色/蓝色/绿色/灰色/琥珀色)
- 更新 AccountUsageCell.vue
- 添加 Google One tier 显示逻辑
- 根据 oauth_type 区分显示方式
- 添加国际化翻译 (en.ts, zh.ts)
- aiPremium, standard, basic, free, personal, unlimited
## Tier 推断规则
- >= 2TB: AI Premium
- >= 200GB: Google One Standard
- >= 100GB: Google One Basic
- >= 15GB: Free
- > 100TB: Unlimited (G Suite legacy)
- 其他/失败: Unknown (显示为 Personal)
## 优雅降级
- Drive API 失败时使用 GOOGLE_ONE_UNKNOWN
- 不阻断 OAuth 流程
- 24小时缓存避免频繁调用
## 测试
- ✅ 后端编译成功
- ✅ 前端构建成功
- ✅ 所有代码符合现有规范
2025-12-31 21:45:24 -08:00
|
|
|
if oauthType != "code_assist" && oauthType != "google_one" && oauthType != "ai_studio" {
|
|
|
|
|
response.BadRequest(c, "Invalid oauth_type: must be 'code_assist', 'google_one', or 'ai_studio'")
|
2025-12-25 21:24:53 -08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-25 06:45:03 -08:00
|
|
|
tokenInfo, err := h.geminiOAuthService.ExchangeCode(c.Request.Context(), &service.GeminiExchangeCodeInput{
|
2025-12-25 21:24:53 -08:00
|
|
|
SessionID: req.SessionID,
|
|
|
|
|
State: req.State,
|
|
|
|
|
Code: req.Code,
|
|
|
|
|
ProxyID: req.ProxyID,
|
|
|
|
|
OAuthType: oauthType,
|
2025-12-25 06:45:03 -08:00
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.BadRequest(c, "Failed to exchange code: "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response.Success(c, tokenInfo)
|
|
|
|
|
}
|
2025-12-25 21:24:53 -08:00
|
|
|
|
|
|
|
|
func deriveGeminiRedirectURI(c *gin.Context) string {
|
|
|
|
|
origin := strings.TrimSpace(c.GetHeader("Origin"))
|
|
|
|
|
if origin != "" {
|
|
|
|
|
return strings.TrimRight(origin, "/") + "/auth/callback"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scheme := "http"
|
|
|
|
|
if c.Request.TLS != nil {
|
|
|
|
|
scheme = "https"
|
|
|
|
|
}
|
|
|
|
|
if xfProto := strings.TrimSpace(c.GetHeader("X-Forwarded-Proto")); xfProto != "" {
|
|
|
|
|
scheme = strings.TrimSpace(strings.Split(xfProto, ",")[0])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
host := strings.TrimSpace(c.Request.Host)
|
|
|
|
|
if xfHost := strings.TrimSpace(c.GetHeader("X-Forwarded-Host")); xfHost != "" {
|
|
|
|
|
host = strings.TrimSpace(strings.Split(xfHost, ",")[0])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fmt.Sprintf("%s://%s/auth/callback", scheme, host)
|
|
|
|
|
}
|