From 632318ad33cd92ec7adb36a3d37bff12294667c8 Mon Sep 17 00:00:00 2001 From: ianshaw Date: Thu, 25 Dec 2025 23:52:02 -0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=E6=B7=BB=E5=8A=A0=20OAuth=20?= =?UTF-8?q?=E8=83=BD=E5=8A=9B=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E8=BF=9B=20OAuth=20=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handler 改进: - 添加 GET /api/v1/admin/gemini/oauth/capabilities 接口 - 简化 GenerateAuthURL,redirect_uri 由服务层决定 Repository 改进: - ExchangeCode/RefreshToken 根据 oauthType 选择正确的 OAuth 客户端 - Code Assist 始终使用内置客户端,AI Studio 使用用户配置的客户端 --- .../handler/admin/gemini_oauth_handler.go | 14 +++++---- .../repository/gemini_oauth_client.go | 29 ++++++++++++++----- backend/internal/server/router.go | 1 + 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/backend/internal/handler/admin/gemini_oauth_handler.go b/backend/internal/handler/admin/gemini_oauth_handler.go index f6827735..4440aa21 100644 --- a/backend/internal/handler/admin/gemini_oauth_handler.go +++ b/backend/internal/handler/admin/gemini_oauth_handler.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/Wei-Shaw/sub2api/internal/pkg/geminicli" "github.com/Wei-Shaw/sub2api/internal/pkg/response" "github.com/Wei-Shaw/sub2api/internal/service" @@ -19,6 +18,12 @@ func NewGeminiOAuthHandler(geminiOAuthService *service.GeminiOAuthService) *Gemi return &GeminiOAuthHandler{geminiOAuthService: geminiOAuthService} } +// GET /api/v1/admin/gemini/oauth/capabilities +func (h *GeminiOAuthHandler) GetCapabilities(c *gin.Context) { + cfg := h.geminiOAuthService.GetOAuthConfig() + response.Success(c, cfg) +} + type GeminiGenerateAuthURLRequest struct { ProxyID *int64 `json:"proxy_id"` ProjectID string `json:"project_id"` @@ -46,12 +51,9 @@ func (h *GeminiOAuthHandler) GenerateAuthURL(c *gin.Context) { return } + // 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. redirectURI := deriveGeminiRedirectURI(c) - if oauthType == "ai_studio" { - // AI Studio OAuth uses a localhost redirect URI to support the "copy/paste callback URL" - // flow (no server-side callback endpoint needed). - redirectURI = geminicli.AIStudioOAuthRedirectURI - } result, err := h.geminiOAuthService.GenerateAuthURL(c.Request.Context(), req.ProxyID, redirectURI, req.ProjectID, oauthType) if err != nil { msg := err.Error() diff --git a/backend/internal/repository/gemini_oauth_client.go b/backend/internal/repository/gemini_oauth_client.go index c13cfda6..4e9bae3e 100644 --- a/backend/internal/repository/gemini_oauth_client.go +++ b/backend/internal/repository/gemini_oauth_client.go @@ -25,15 +25,23 @@ func NewGeminiOAuthClient(cfg *config.Config) service.GeminiOAuthClient { } } -func (c *geminiOAuthClient) ExchangeCode(ctx context.Context, code, codeVerifier, redirectURI, proxyURL string) (*geminicli.TokenResponse, error) { +func (c *geminiOAuthClient) ExchangeCode(ctx context.Context, oauthType, code, codeVerifier, redirectURI, proxyURL string) (*geminicli.TokenResponse, error) { client := createGeminiReqClient(proxyURL) - // Both Code Assist and AI Studio OAuth use the same token endpoint and OAuth client. - oauthCfg, err := geminicli.EffectiveOAuthConfig(geminicli.OAuthConfig{ + // Use different OAuth clients based on oauthType: + // - code_assist: always use built-in Gemini CLI OAuth client (public) + // - ai_studio: requires a user-provided OAuth client + oauthCfgInput := geminicli.OAuthConfig{ ClientID: c.cfg.Gemini.OAuth.ClientID, ClientSecret: c.cfg.Gemini.OAuth.ClientSecret, Scopes: c.cfg.Gemini.OAuth.Scopes, - }, "code_assist") + } + if oauthType == "code_assist" { + oauthCfgInput.ClientID = "" + oauthCfgInput.ClientSecret = "" + } + + oauthCfg, err := geminicli.EffectiveOAuthConfig(oauthCfgInput, oauthType) if err != nil { return nil, err } @@ -61,15 +69,20 @@ func (c *geminiOAuthClient) ExchangeCode(ctx context.Context, code, codeVerifier return &tokenResp, nil } -func (c *geminiOAuthClient) RefreshToken(ctx context.Context, refreshToken, proxyURL string) (*geminicli.TokenResponse, error) { +func (c *geminiOAuthClient) RefreshToken(ctx context.Context, oauthType, refreshToken, proxyURL string) (*geminicli.TokenResponse, error) { client := createGeminiReqClient(proxyURL) - // Both Code Assist and AI Studio OAuth use the same token endpoint and OAuth client. - oauthCfg, err := geminicli.EffectiveOAuthConfig(geminicli.OAuthConfig{ + oauthCfgInput := geminicli.OAuthConfig{ ClientID: c.cfg.Gemini.OAuth.ClientID, ClientSecret: c.cfg.Gemini.OAuth.ClientSecret, Scopes: c.cfg.Gemini.OAuth.Scopes, - }, "code_assist") + } + if oauthType == "code_assist" { + oauthCfgInput.ClientID = "" + oauthCfgInput.ClientSecret = "" + } + + oauthCfg, err := geminicli.EffectiveOAuthConfig(oauthCfgInput, oauthType) if err != nil { return nil, err } diff --git a/backend/internal/server/router.go b/backend/internal/server/router.go index 847e64b1..10e71d1e 100644 --- a/backend/internal/server/router.go +++ b/backend/internal/server/router.go @@ -218,6 +218,7 @@ func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, rep // Gemini OAuth routes gemini := admin.Group("/gemini") { + gemini.GET("/oauth/capabilities", h.Admin.GeminiOAuth.GetCapabilities) gemini.POST("/oauth/auth-url", h.Admin.GeminiOAuth.GenerateAuthURL) gemini.POST("/oauth/exchange-code", h.Admin.GeminiOAuth.ExchangeCode) }