diff --git a/backend/cmd/server/wire.go b/backend/cmd/server/wire.go index 7d6ec065..a74c906a 100644 --- a/backend/cmd/server/wire.go +++ b/backend/cmd/server/wire.go @@ -99,6 +99,10 @@ func provideCleanup( openaiOAuth.Stop() return nil }}, + {"GeminiOAuthService", func() error { + services.GeminiOAuth.Stop() + return nil + }}, {"Redis", func() error { return rdb.Close() }}, diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index e72e5f6e..6ed9897b 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -14,7 +14,6 @@ import ( "github.com/Wei-Shaw/sub2api/internal/infrastructure" "github.com/Wei-Shaw/sub2api/internal/repository" "github.com/Wei-Shaw/sub2api/internal/server" - "github.com/Wei-Shaw/sub2api/internal/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" "gorm.io/gorm" @@ -80,6 +79,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { oAuthService := service.NewOAuthService(proxyRepository, claudeOAuthClient) openAIOAuthClient := repository.NewOpenAIOAuthClient() openAIOAuthService := service.NewOpenAIOAuthService(proxyRepository, openAIOAuthClient) + geminiOAuthClient := repository.NewGeminiOAuthClient(configConfig) + geminiCliCodeAssistClient := repository.NewGeminiCliCodeAssistClient() + geminiOAuthService := service.NewGeminiOAuthService(proxyRepository, geminiOAuthClient, geminiCliCodeAssistClient, configConfig) rateLimitService := service.NewRateLimitService(accountRepository, configConfig) claudeUsageFetcher := repository.NewClaudeUsageFetcher() accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher) @@ -87,10 +89,11 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { accountTestService := service.NewAccountTestService(accountRepository, oAuthService, openAIOAuthService, httpUpstream) concurrencyCache := repository.NewConcurrencyCache(client) concurrencyService := service.NewConcurrencyService(concurrencyCache) - crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService) - accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService) + crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService) + accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService) oAuthHandler := admin.NewOAuthHandler(oAuthService) openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService) + geminiOAuthHandler := admin.NewGeminiOAuthHandler(geminiOAuthService) proxyHandler := admin.NewProxyHandler(adminService) adminRedeemHandler := admin.NewRedeemHandler(adminService) settingHandler := admin.NewSettingHandler(settingService, emailService) @@ -101,7 +104,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { systemHandler := handler.ProvideSystemHandler(updateService) adminSubscriptionHandler := admin.NewSubscriptionHandler(subscriptionService) adminUsageHandler := admin.NewUsageHandler(usageService, apiKeyService, adminService) - adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, proxyHandler, adminRedeemHandler, settingHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler) + adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, proxyHandler, adminRedeemHandler, settingHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler) gatewayCache := repository.NewGatewayCache(client) pricingRemoteClient := repository.NewPricingRemoteClient() pricingService, err := service.ProvidePricingService(configConfig, pricingRemoteClient) @@ -112,18 +115,63 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { identityCache := repository.NewIdentityCache(client) identityService := service.NewIdentityService(identityCache) gatewayService := service.NewGatewayService(accountRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, billingService, rateLimitService, billingCacheService, identityService, httpUpstream) - gatewayHandler := handler.NewGatewayHandler(gatewayService, userService, concurrencyService, billingCacheService) + geminiTokenCache := repository.NewGeminiTokenCache(client) + geminiTokenProvider := service.NewGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService) + geminiMessagesCompatService := service.NewGeminiMessagesCompatService(accountRepository, gatewayCache, geminiTokenProvider, rateLimitService, httpUpstream) + gatewayHandler := handler.NewGatewayHandler(gatewayService, geminiMessagesCompatService, userService, concurrencyService, billingCacheService) openAIGatewayService := service.NewOpenAIGatewayService(accountRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, billingService, rateLimitService, billingCacheService, httpUpstream) openAIGatewayHandler := handler.NewOpenAIGatewayHandler(openAIGatewayService, concurrencyService, billingCacheService) handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo) handlers := handler.ProvideHandlers(authHandler, userHandler, apiKeyHandler, usageHandler, redeemHandler, subscriptionHandler, adminHandlers, gatewayHandler, openAIGatewayHandler, handlerSettingHandler) - jwtAuthMiddleware := middleware.NewJWTAuthMiddleware(authService, userService) - adminAuthMiddleware := middleware.NewAdminAuthMiddleware(authService, userService, settingService) - apiKeyAuthMiddleware := middleware.NewApiKeyAuthMiddleware(apiKeyService, subscriptionService) - engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware) + groupService := service.NewGroupService(groupRepository) + accountService := service.NewAccountService(accountRepository, groupRepository) + proxyService := service.NewProxyService(proxyRepository) + tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig) + services := &service.Services{ + Auth: authService, + User: userService, + ApiKey: apiKeyService, + Group: groupService, + Account: accountService, + Proxy: proxyService, + Redeem: redeemService, + Usage: usageService, + Pricing: pricingService, + Billing: billingService, + BillingCache: billingCacheService, + Admin: adminService, + Gateway: gatewayService, + OpenAIGateway: openAIGatewayService, + OAuth: oAuthService, + OpenAIOAuth: openAIOAuthService, + GeminiOAuth: geminiOAuthService, + RateLimit: rateLimitService, + AccountUsage: accountUsageService, + AccountTest: accountTestService, + Setting: settingService, + Email: emailService, + EmailQueue: emailQueueService, + Turnstile: turnstileService, + Subscription: subscriptionService, + Concurrency: concurrencyService, + Identity: identityService, + Update: updateService, + TokenRefresh: tokenRefreshService, + } + repositories := &repository.Repositories{ + User: userRepository, + ApiKey: apiKeyRepository, + Group: groupRepository, + Account: accountRepository, + Proxy: proxyRepository, + RedeemCode: redeemCodeRepository, + UsageLog: usageLogRepository, + Setting: settingRepository, + UserSubscription: userSubscriptionRepository, + } + engine := server.ProvideRouter(configConfig, handlers, services, repositories) httpServer := server.ProvideHTTPServer(configConfig, engine) - tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, configConfig) - v := provideCleanup(db, client, tokenRefreshService, pricingService, emailQueueService, oAuthService, openAIOAuthService) + v := provideCleanup(db, client, services) application := &Application{ Server: httpServer, Cleanup: v, @@ -148,11 +196,7 @@ func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo { func provideCleanup( db *gorm.DB, rdb *redis.Client, - tokenRefresh *service.TokenRefreshService, - pricing *service.PricingService, - emailQueue *service.EmailQueueService, - oauth *service.OAuthService, - openaiOAuth *service.OpenAIOAuthService, + services *service.Services, ) func() { return func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -163,23 +207,23 @@ func provideCleanup( fn func() error }{ {"TokenRefreshService", func() error { - tokenRefresh.Stop() + services.TokenRefresh.Stop() return nil }}, {"PricingService", func() error { - pricing.Stop() + services.Pricing.Stop() return nil }}, {"EmailQueueService", func() error { - emailQueue.Stop() + services.EmailQueue.Stop() return nil }}, {"OAuthService", func() error { - oauth.Stop() + services.OAuth.Stop() return nil }}, {"OpenAIOAuthService", func() error { - openaiOAuth.Stop() + services.OpenAIOAuth.Stop() return nil }}, {"Redis", func() error { diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 8ecb4326..3b6827e9 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -30,6 +30,7 @@ type AccountHandler struct { adminService service.AdminService oauthService *service.OAuthService openaiOAuthService *service.OpenAIOAuthService + geminiOAuthService *service.GeminiOAuthService rateLimitService *service.RateLimitService accountUsageService *service.AccountUsageService accountTestService *service.AccountTestService @@ -42,6 +43,7 @@ func NewAccountHandler( adminService service.AdminService, oauthService *service.OAuthService, openaiOAuthService *service.OpenAIOAuthService, + geminiOAuthService *service.GeminiOAuthService, rateLimitService *service.RateLimitService, accountUsageService *service.AccountUsageService, accountTestService *service.AccountTestService, @@ -52,6 +54,7 @@ func NewAccountHandler( adminService: adminService, oauthService: oauthService, openaiOAuthService: openaiOAuthService, + geminiOAuthService: geminiOAuthService, rateLimitService: rateLimitService, accountUsageService: accountUsageService, accountTestService: accountTestService, @@ -345,6 +348,19 @@ func (h *AccountHandler) Refresh(c *gin.Context) { newCredentials[k] = v } } + } else if account.Platform == model.PlatformGemini { + tokenInfo, err := h.geminiOAuthService.RefreshAccountToken(c.Request.Context(), account) + if err != nil { + response.InternalError(c, "Failed to refresh credentials: "+err.Error()) + return + } + + newCredentials = h.geminiOAuthService.BuildAccountCredentials(tokenInfo) + for k, v := range account.Credentials { + if _, exists := newCredentials[k]; !exists { + newCredentials[k] = v + } + } } else { // Use Anthropic/Claude OAuth service to refresh token tokenInfo, err := h.oauthService.RefreshAccountToken(c.Request.Context(), account) diff --git a/backend/internal/handler/admin/gemini_oauth_handler.go b/backend/internal/handler/admin/gemini_oauth_handler.go new file mode 100644 index 00000000..4d39700b --- /dev/null +++ b/backend/internal/handler/admin/gemini_oauth_handler.go @@ -0,0 +1,71 @@ +package admin + +import ( + "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} +} + +type GeminiGenerateAuthURLRequest struct { + ProxyID *int64 `json:"proxy_id"` + RedirectURI string `json:"redirect_uri" binding:"required"` +} + +// 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 + } + + result, err := h.geminiOAuthService.GenerateAuthURL(c.Request.Context(), req.ProxyID, req.RedirectURI) + if err != nil { + response.InternalError(c, "Failed to generate auth URL: "+err.Error()) + return + } + + response.Success(c, result) +} + +type GeminiExchangeCodeRequest struct { + SessionID string `json:"session_id" binding:"required"` + State string `json:"state" binding:"required"` + Code string `json:"code" binding:"required"` + RedirectURI string `json:"redirect_uri" binding:"required"` + ProxyID *int64 `json:"proxy_id"` +} + +// 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 + } + + tokenInfo, err := h.geminiOAuthService.ExchangeCode(c.Request.Context(), &service.GeminiExchangeCodeInput{ + SessionID: req.SessionID, + State: req.State, + Code: req.Code, + RedirectURI: req.RedirectURI, + ProxyID: req.ProxyID, + }) + if err != nil { + response.BadRequest(c, "Failed to exchange code: "+err.Error()) + return + } + + response.Success(c, tokenInfo) +} diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index bfb8b6fd..a980dfb1 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -22,15 +22,23 @@ import ( // GatewayHandler handles API gateway requests type GatewayHandler struct { gatewayService *service.GatewayService + geminiCompatService *service.GeminiMessagesCompatService userService *service.UserService billingCacheService *service.BillingCacheService concurrencyHelper *ConcurrencyHelper } // NewGatewayHandler creates a new GatewayHandler -func NewGatewayHandler(gatewayService *service.GatewayService, userService *service.UserService, concurrencyService *service.ConcurrencyService, billingCacheService *service.BillingCacheService) *GatewayHandler { +func NewGatewayHandler( + gatewayService *service.GatewayService, + geminiCompatService *service.GeminiMessagesCompatService, + userService *service.UserService, + concurrencyService *service.ConcurrencyService, + billingCacheService *service.BillingCacheService, +) *GatewayHandler { return &GatewayHandler{ gatewayService: gatewayService, + geminiCompatService: geminiCompatService, userService: userService, billingCacheService: billingCacheService, concurrencyHelper: NewConcurrencyHelper(concurrencyService, SSEPingFormatClaude), @@ -115,8 +123,18 @@ func (h *GatewayHandler) Messages(c *gin.Context) { // 计算粘性会话hash sessionHash := h.gatewayService.GenerateSessionHash(body) + platform := "" + if apiKey.Group != nil { + platform = apiKey.Group.Platform + } + // 选择支持该模型的账号 - account, err := h.gatewayService.SelectAccountForModel(c.Request.Context(), apiKey.GroupID, sessionHash, req.Model) + var account *model.Account + if platform == model.PlatformGemini { + account, err = h.geminiCompatService.SelectAccountForModel(c.Request.Context(), apiKey.GroupID, sessionHash, req.Model) + } else { + account, err = h.gatewayService.SelectAccountForModel(c.Request.Context(), apiKey.GroupID, sessionHash, req.Model) + } if err != nil { h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts: "+err.Error(), streamStarted) return @@ -144,7 +162,12 @@ func (h *GatewayHandler) Messages(c *gin.Context) { } // 转发请求 - result, err := h.gatewayService.Forward(c.Request.Context(), c, account, body) + var result *service.ForwardResult + if platform == model.PlatformGemini { + result, err = h.geminiCompatService.Forward(c.Request.Context(), c, account, body) + } else { + result, err = h.gatewayService.Forward(c.Request.Context(), c, account, body) + } if err != nil { // 错误响应已在Forward中处理,这里只记录日志 log.Printf("Forward request failed: %v", err) diff --git a/backend/internal/handler/handler.go b/backend/internal/handler/handler.go index 11c9dcf1..af28bc1f 100644 --- a/backend/internal/handler/handler.go +++ b/backend/internal/handler/handler.go @@ -12,6 +12,7 @@ type AdminHandlers struct { Account *admin.AccountHandler OAuth *admin.OAuthHandler OpenAIOAuth *admin.OpenAIOAuthHandler + GeminiOAuth *admin.GeminiOAuthHandler Proxy *admin.ProxyHandler Redeem *admin.RedeemHandler Setting *admin.SettingHandler diff --git a/backend/internal/handler/wire.go b/backend/internal/handler/wire.go index a37cb3e6..f6e2c031 100644 --- a/backend/internal/handler/wire.go +++ b/backend/internal/handler/wire.go @@ -15,6 +15,7 @@ func ProvideAdminHandlers( accountHandler *admin.AccountHandler, oauthHandler *admin.OAuthHandler, openaiOAuthHandler *admin.OpenAIOAuthHandler, + geminiOAuthHandler *admin.GeminiOAuthHandler, proxyHandler *admin.ProxyHandler, redeemHandler *admin.RedeemHandler, settingHandler *admin.SettingHandler, @@ -29,6 +30,7 @@ func ProvideAdminHandlers( Account: accountHandler, OAuth: oauthHandler, OpenAIOAuth: openaiOAuthHandler, + GeminiOAuth: geminiOAuthHandler, Proxy: proxyHandler, Redeem: redeemHandler, Setting: settingHandler, @@ -95,6 +97,7 @@ var ProviderSet = wire.NewSet( admin.NewAccountHandler, admin.NewOAuthHandler, admin.NewOpenAIOAuthHandler, + admin.NewGeminiOAuthHandler, admin.NewProxyHandler, admin.NewRedeemHandler, admin.NewSettingHandler, diff --git a/backend/internal/repository/wire.go b/backend/internal/repository/wire.go index ceeb82fc..53d42d90 100644 --- a/backend/internal/repository/wire.go +++ b/backend/internal/repository/wire.go @@ -25,6 +25,7 @@ var ProviderSet = wire.NewSet( NewIdentityCache, NewRedeemCache, NewUpdateCache, + NewGeminiTokenCache, // HTTP service ports (DI Strategy A: return interface directly) NewTurnstileVerifier, @@ -35,4 +36,6 @@ var ProviderSet = wire.NewSet( NewClaudeOAuthClient, NewHTTPUpstream, NewOpenAIOAuthClient, + NewGeminiOAuthClient, + NewGeminiCliCodeAssistClient, ) diff --git a/backend/internal/server/router.go b/backend/internal/server/router.go index 226fe99b..d1a73c43 100644 --- a/backend/internal/server/router.go +++ b/backend/internal/server/router.go @@ -1,54 +1,319 @@ package server import ( + "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/handler" - middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware" - "github.com/Wei-Shaw/sub2api/internal/server/routes" + "github.com/Wei-Shaw/sub2api/internal/middleware" + "github.com/Wei-Shaw/sub2api/internal/repository" + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/Wei-Shaw/sub2api/internal/web" + "net/http" "github.com/gin-gonic/gin" ) // SetupRouter 配置路由器中间件和路由 -func SetupRouter( - r *gin.Engine, - handlers *handler.Handlers, - jwtAuth middleware2.JWTAuthMiddleware, - adminAuth middleware2.AdminAuthMiddleware, - apiKeyAuth middleware2.ApiKeyAuthMiddleware, -) *gin.Engine { +func SetupRouter(r *gin.Engine, cfg *config.Config, handlers *handler.Handlers, services *service.Services, repos *repository.Repositories) *gin.Engine { // 应用中间件 - r.Use(middleware2.Logger()) - r.Use(middleware2.CORS()) + r.Use(middleware.Logger()) + r.Use(middleware.CORS()) + + // 注册路由 + registerRoutes(r, handlers, services, repos) // Serve embedded frontend if available if web.HasEmbeddedFrontend() { r.Use(web.ServeEmbeddedFrontend()) } - // 注册路由 - registerRoutes(r, handlers, jwtAuth, adminAuth, apiKeyAuth) - return r } // registerRoutes 注册所有 HTTP 路由 -func registerRoutes( - r *gin.Engine, - h *handler.Handlers, - jwtAuth middleware2.JWTAuthMiddleware, - adminAuth middleware2.AdminAuthMiddleware, - apiKeyAuth middleware2.ApiKeyAuthMiddleware, -) { - // 通用路由(健康检查、状态等) - routes.RegisterCommonRoutes(r) +func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, repos *repository.Repositories) { + // 健康检查 + r.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + + // Claude Code 遥测日志(忽略,直接返回200) + r.POST("/api/event_logging/batch", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + // Setup status endpoint (always returns needs_setup: false in normal mode) + // This is used by the frontend to detect when the service has restarted after setup + r.GET("/setup/status", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "data": gin.H{ + "needs_setup": false, + "step": "completed", + }, + }) + }) // API v1 v1 := r.Group("/api/v1") + { + // 公开接口 + auth := v1.Group("/auth") + { + auth.POST("/register", h.Auth.Register) + auth.POST("/login", h.Auth.Login) + auth.POST("/send-verify-code", h.Auth.SendVerifyCode) + } - // 注册各模块路由 - routes.RegisterAuthRoutes(v1, h, jwtAuth) - routes.RegisterUserRoutes(v1, h, jwtAuth) - routes.RegisterAdminRoutes(v1, h, adminAuth) - routes.RegisterGatewayRoutes(r, h, apiKeyAuth) + // 公开设置(无需认证) + settings := v1.Group("/settings") + { + settings.GET("/public", h.Setting.GetPublicSettings) + } + + // 需要认证的接口 + authenticated := v1.Group("") + authenticated.Use(middleware.JWTAuth(s.Auth, repos.User)) + { + // 当前用户信息 + authenticated.GET("/auth/me", h.Auth.GetCurrentUser) + + // 用户接口 + user := authenticated.Group("/user") + { + user.GET("/profile", h.User.GetProfile) + user.PUT("/password", h.User.ChangePassword) + user.PUT("", h.User.UpdateProfile) + } + + // API Key管理 + keys := authenticated.Group("/keys") + { + keys.GET("", h.APIKey.List) + keys.GET("/:id", h.APIKey.GetByID) + keys.POST("", h.APIKey.Create) + keys.PUT("/:id", h.APIKey.Update) + keys.DELETE("/:id", h.APIKey.Delete) + } + + // 用户可用分组(非管理员接口) + groups := authenticated.Group("/groups") + { + groups.GET("/available", h.APIKey.GetAvailableGroups) + } + + // 使用记录 + usage := authenticated.Group("/usage") + { + usage.GET("", h.Usage.List) + usage.GET("/:id", h.Usage.GetByID) + usage.GET("/stats", h.Usage.Stats) + // User dashboard endpoints + usage.GET("/dashboard/stats", h.Usage.DashboardStats) + usage.GET("/dashboard/trend", h.Usage.DashboardTrend) + usage.GET("/dashboard/models", h.Usage.DashboardModels) + usage.POST("/dashboard/api-keys-usage", h.Usage.DashboardApiKeysUsage) + } + + // 卡密兑换 + redeem := authenticated.Group("/redeem") + { + redeem.POST("", h.Redeem.Redeem) + redeem.GET("/history", h.Redeem.GetHistory) + } + + // 用户订阅 + subscriptions := authenticated.Group("/subscriptions") + { + subscriptions.GET("", h.Subscription.List) + subscriptions.GET("/active", h.Subscription.GetActive) + subscriptions.GET("/progress", h.Subscription.GetProgress) + subscriptions.GET("/summary", h.Subscription.GetSummary) + } + } + + // 管理员接口 + admin := v1.Group("/admin") + admin.Use(middleware.AdminAuth(s.Auth, repos.User, s.Setting)) + { + // 仪表盘 + dashboard := admin.Group("/dashboard") + { + dashboard.GET("/stats", h.Admin.Dashboard.GetStats) + dashboard.GET("/realtime", h.Admin.Dashboard.GetRealtimeMetrics) + dashboard.GET("/trend", h.Admin.Dashboard.GetUsageTrend) + dashboard.GET("/models", h.Admin.Dashboard.GetModelStats) + dashboard.GET("/api-keys-trend", h.Admin.Dashboard.GetApiKeyUsageTrend) + dashboard.GET("/users-trend", h.Admin.Dashboard.GetUserUsageTrend) + dashboard.POST("/users-usage", h.Admin.Dashboard.GetBatchUsersUsage) + dashboard.POST("/api-keys-usage", h.Admin.Dashboard.GetBatchApiKeysUsage) + } + + // 用户管理 + users := admin.Group("/users") + { + users.GET("", h.Admin.User.List) + users.GET("/:id", h.Admin.User.GetByID) + users.POST("", h.Admin.User.Create) + users.PUT("/:id", h.Admin.User.Update) + users.DELETE("/:id", h.Admin.User.Delete) + users.POST("/:id/balance", h.Admin.User.UpdateBalance) + users.GET("/:id/api-keys", h.Admin.User.GetUserAPIKeys) + users.GET("/:id/usage", h.Admin.User.GetUserUsage) + } + + // 分组管理 + groups := admin.Group("/groups") + { + groups.GET("", h.Admin.Group.List) + groups.GET("/all", h.Admin.Group.GetAll) + groups.GET("/:id", h.Admin.Group.GetByID) + groups.POST("", h.Admin.Group.Create) + groups.PUT("/:id", h.Admin.Group.Update) + groups.DELETE("/:id", h.Admin.Group.Delete) + groups.GET("/:id/stats", h.Admin.Group.GetStats) + groups.GET("/:id/api-keys", h.Admin.Group.GetGroupAPIKeys) + } + + // 账号管理 + accounts := admin.Group("/accounts") + { + accounts.GET("", h.Admin.Account.List) + accounts.GET("/:id", h.Admin.Account.GetByID) + accounts.POST("", h.Admin.Account.Create) + accounts.POST("/sync/crs", h.Admin.Account.SyncFromCRS) + accounts.PUT("/:id", h.Admin.Account.Update) + accounts.DELETE("/:id", h.Admin.Account.Delete) + accounts.POST("/:id/test", h.Admin.Account.Test) + accounts.POST("/:id/refresh", h.Admin.Account.Refresh) + accounts.GET("/:id/stats", h.Admin.Account.GetStats) + accounts.POST("/:id/clear-error", h.Admin.Account.ClearError) + accounts.GET("/:id/usage", h.Admin.Account.GetUsage) + accounts.GET("/:id/today-stats", h.Admin.Account.GetTodayStats) + accounts.POST("/:id/clear-rate-limit", h.Admin.Account.ClearRateLimit) + accounts.POST("/:id/schedulable", h.Admin.Account.SetSchedulable) + accounts.GET("/:id/models", h.Admin.Account.GetAvailableModels) + accounts.POST("/batch", h.Admin.Account.BatchCreate) + accounts.POST("/batch-update-credentials", h.Admin.Account.BatchUpdateCredentials) + accounts.POST("/bulk-update", h.Admin.Account.BulkUpdate) + + // Claude OAuth routes + accounts.POST("/generate-auth-url", h.Admin.OAuth.GenerateAuthURL) + accounts.POST("/generate-setup-token-url", h.Admin.OAuth.GenerateSetupTokenURL) + accounts.POST("/exchange-code", h.Admin.OAuth.ExchangeCode) + accounts.POST("/exchange-setup-token-code", h.Admin.OAuth.ExchangeSetupTokenCode) + accounts.POST("/cookie-auth", h.Admin.OAuth.CookieAuth) + accounts.POST("/setup-token-cookie-auth", h.Admin.OAuth.SetupTokenCookieAuth) + } + + // OpenAI OAuth routes + openai := admin.Group("/openai") + { + openai.POST("/generate-auth-url", h.Admin.OpenAIOAuth.GenerateAuthURL) + openai.POST("/exchange-code", h.Admin.OpenAIOAuth.ExchangeCode) + openai.POST("/refresh-token", h.Admin.OpenAIOAuth.RefreshToken) + openai.POST("/accounts/:id/refresh", h.Admin.OpenAIOAuth.RefreshAccountToken) + openai.POST("/create-from-oauth", h.Admin.OpenAIOAuth.CreateAccountFromOAuth) + } + + // Gemini OAuth routes + gemini := admin.Group("/gemini") + { + gemini.POST("/oauth/auth-url", h.Admin.GeminiOAuth.GenerateAuthURL) + gemini.POST("/oauth/exchange-code", h.Admin.GeminiOAuth.ExchangeCode) + } + + // 代理管理 + proxies := admin.Group("/proxies") + { + proxies.GET("", h.Admin.Proxy.List) + proxies.GET("/all", h.Admin.Proxy.GetAll) + proxies.GET("/:id", h.Admin.Proxy.GetByID) + proxies.POST("", h.Admin.Proxy.Create) + proxies.PUT("/:id", h.Admin.Proxy.Update) + proxies.DELETE("/:id", h.Admin.Proxy.Delete) + proxies.POST("/:id/test", h.Admin.Proxy.Test) + proxies.GET("/:id/stats", h.Admin.Proxy.GetStats) + proxies.GET("/:id/accounts", h.Admin.Proxy.GetProxyAccounts) + proxies.POST("/batch", h.Admin.Proxy.BatchCreate) + } + + // 卡密管理 + codes := admin.Group("/redeem-codes") + { + codes.GET("", h.Admin.Redeem.List) + codes.GET("/stats", h.Admin.Redeem.GetStats) + codes.GET("/export", h.Admin.Redeem.Export) + codes.GET("/:id", h.Admin.Redeem.GetByID) + codes.POST("/generate", h.Admin.Redeem.Generate) + codes.DELETE("/:id", h.Admin.Redeem.Delete) + codes.POST("/batch-delete", h.Admin.Redeem.BatchDelete) + codes.POST("/:id/expire", h.Admin.Redeem.Expire) + } + + // 系统设置 + adminSettings := admin.Group("/settings") + { + adminSettings.GET("", h.Admin.Setting.GetSettings) + adminSettings.PUT("", h.Admin.Setting.UpdateSettings) + adminSettings.POST("/test-smtp", h.Admin.Setting.TestSmtpConnection) + adminSettings.POST("/send-test-email", h.Admin.Setting.SendTestEmail) + // Admin API Key 管理 + adminSettings.GET("/admin-api-key", h.Admin.Setting.GetAdminApiKey) + adminSettings.POST("/admin-api-key/regenerate", h.Admin.Setting.RegenerateAdminApiKey) + adminSettings.DELETE("/admin-api-key", h.Admin.Setting.DeleteAdminApiKey) + } + + // 系统管理 + system := admin.Group("/system") + { + system.GET("/version", h.Admin.System.GetVersion) + system.GET("/check-updates", h.Admin.System.CheckUpdates) + system.POST("/update", h.Admin.System.PerformUpdate) + system.POST("/rollback", h.Admin.System.Rollback) + system.POST("/restart", h.Admin.System.RestartService) + } + + // 订阅管理 + subscriptions := admin.Group("/subscriptions") + { + subscriptions.GET("", h.Admin.Subscription.List) + subscriptions.GET("/:id", h.Admin.Subscription.GetByID) + subscriptions.GET("/:id/progress", h.Admin.Subscription.GetProgress) + subscriptions.POST("/assign", h.Admin.Subscription.Assign) + subscriptions.POST("/bulk-assign", h.Admin.Subscription.BulkAssign) + subscriptions.POST("/:id/extend", h.Admin.Subscription.Extend) + subscriptions.DELETE("/:id", h.Admin.Subscription.Revoke) + } + + // 分组下的订阅列表 + admin.GET("/groups/:id/subscriptions", h.Admin.Subscription.ListByGroup) + + // 用户下的订阅列表 + admin.GET("/users/:id/subscriptions", h.Admin.Subscription.ListByUser) + + // 使用记录管理 + usage := admin.Group("/usage") + { + usage.GET("", h.Admin.Usage.List) + usage.GET("/stats", h.Admin.Usage.Stats) + usage.GET("/search-users", h.Admin.Usage.SearchUsers) + usage.GET("/search-api-keys", h.Admin.Usage.SearchApiKeys) + } + } + } + + // API网关(Claude API兼容) + gateway := r.Group("/v1") + gateway.Use(middleware.ApiKeyAuthWithSubscription(s.ApiKey, s.Subscription)) + { + gateway.POST("/messages", h.Gateway.Messages) + gateway.POST("/messages/count_tokens", h.Gateway.CountTokens) + gateway.GET("/models", h.Gateway.Models) + gateway.GET("/usage", h.Gateway.Usage) + // OpenAI Responses API + gateway.POST("/responses", h.OpenAIGateway.Responses) + } + + // OpenAI Responses API(不带v1前缀的别名) + r.POST("/responses", middleware.ApiKeyAuthWithSubscription(s.ApiKey, s.Subscription), h.OpenAIGateway.Responses) } diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go index 02ef2392..d60ec737 100644 --- a/backend/internal/service/wire.go +++ b/backend/internal/service/wire.go @@ -36,9 +36,10 @@ func ProvideTokenRefreshService( accountRepo AccountRepository, oauthService *OAuthService, openaiOAuthService *OpenAIOAuthService, + geminiOAuthService *GeminiOAuthService, cfg *config.Config, ) *TokenRefreshService { - svc := NewTokenRefreshService(accountRepo, oauthService, openaiOAuthService, cfg) + svc := NewTokenRefreshService(accountRepo, oauthService, openaiOAuthService, geminiOAuthService, cfg) svc.Start() return svc } @@ -63,6 +64,9 @@ var ProviderSet = wire.NewSet( NewOpenAIGatewayService, NewOAuthService, NewOpenAIOAuthService, + NewGeminiOAuthService, + NewGeminiTokenProvider, + NewGeminiMessagesCompatService, NewRateLimitService, NewAccountUsageService, NewAccountTestService, diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index d534d3d6..59a91969 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -16,7 +16,7 @@ services: # Sub2API Application # =========================================================================== sub2api: - image: weishaw/sub2api:latest + image: sub2api:latest container_name: sub2api restart: unless-stopped ports: