feat: 添加 Antigravity (Cloud AI Companion) OAuth 授权支持
This commit is contained in:
@@ -29,26 +29,26 @@ type Application struct {
|
|||||||
|
|
||||||
func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||||
wire.Build(
|
wire.Build(
|
||||||
// 基础设施层 ProviderSets
|
// Infrastructure layer ProviderSets
|
||||||
config.ProviderSet,
|
config.ProviderSet,
|
||||||
infrastructure.ProviderSet,
|
infrastructure.ProviderSet,
|
||||||
|
|
||||||
// 业务层 ProviderSets
|
// Business layer ProviderSets
|
||||||
repository.ProviderSet,
|
repository.ProviderSet,
|
||||||
service.ProviderSet,
|
service.ProviderSet,
|
||||||
middleware.ProviderSet,
|
middleware.ProviderSet,
|
||||||
handler.ProviderSet,
|
handler.ProviderSet,
|
||||||
|
|
||||||
// 服务器层 ProviderSet
|
// Server layer ProviderSet
|
||||||
server.ProviderSet,
|
server.ProviderSet,
|
||||||
|
|
||||||
// BuildInfo provider
|
// BuildInfo provider
|
||||||
provideServiceBuildInfo,
|
provideServiceBuildInfo,
|
||||||
|
|
||||||
// 清理函数提供者
|
// Cleanup function provider
|
||||||
provideCleanup,
|
provideCleanup,
|
||||||
|
|
||||||
// 应用程序结构体
|
// Application struct
|
||||||
wire.Struct(new(Application), "Server", "Cleanup"),
|
wire.Struct(new(Application), "Server", "Cleanup"),
|
||||||
)
|
)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -70,6 +70,7 @@ func provideCleanup(
|
|||||||
oauth *service.OAuthService,
|
oauth *service.OAuthService,
|
||||||
openaiOAuth *service.OpenAIOAuthService,
|
openaiOAuth *service.OpenAIOAuthService,
|
||||||
geminiOAuth *service.GeminiOAuthService,
|
geminiOAuth *service.GeminiOAuthService,
|
||||||
|
antigravityOAuth *service.AntigravityOAuthService,
|
||||||
) func() {
|
) func() {
|
||||||
return func() {
|
return func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
@@ -104,6 +105,10 @@ func provideCleanup(
|
|||||||
geminiOAuth.Stop()
|
geminiOAuth.Stop()
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"AntigravityOAuthService", func() error {
|
||||||
|
antigravityOAuth.Stop()
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"Redis", func() error {
|
{"Redis", func() error {
|
||||||
return rdb.Close()
|
return rdb.Close()
|
||||||
}},
|
}},
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
oAuthHandler := admin.NewOAuthHandler(oAuthService)
|
oAuthHandler := admin.NewOAuthHandler(oAuthService)
|
||||||
openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService)
|
openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService)
|
||||||
geminiOAuthHandler := admin.NewGeminiOAuthHandler(geminiOAuthService)
|
geminiOAuthHandler := admin.NewGeminiOAuthHandler(geminiOAuthService)
|
||||||
|
antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository)
|
||||||
|
antigravityOAuthHandler := admin.NewAntigravityOAuthHandler(antigravityOAuthService)
|
||||||
proxyHandler := admin.NewProxyHandler(adminService)
|
proxyHandler := admin.NewProxyHandler(adminService)
|
||||||
adminRedeemHandler := admin.NewRedeemHandler(adminService)
|
adminRedeemHandler := admin.NewRedeemHandler(adminService)
|
||||||
settingHandler := admin.NewSettingHandler(settingService, emailService)
|
settingHandler := admin.NewSettingHandler(settingService, emailService)
|
||||||
@@ -107,7 +109,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
systemHandler := handler.ProvideSystemHandler(updateService)
|
systemHandler := handler.ProvideSystemHandler(updateService)
|
||||||
adminSubscriptionHandler := admin.NewSubscriptionHandler(subscriptionService)
|
adminSubscriptionHandler := admin.NewSubscriptionHandler(subscriptionService)
|
||||||
adminUsageHandler := admin.NewUsageHandler(usageService, apiKeyService, adminService)
|
adminUsageHandler := admin.NewUsageHandler(usageService, apiKeyService, adminService)
|
||||||
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, proxyHandler, adminRedeemHandler, settingHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler)
|
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, settingHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler)
|
||||||
gatewayCache := repository.NewGatewayCache(client)
|
gatewayCache := repository.NewGatewayCache(client)
|
||||||
pricingRemoteClient := repository.NewPricingRemoteClient()
|
pricingRemoteClient := repository.NewPricingRemoteClient()
|
||||||
pricingService, err := service.ProvidePricingService(configConfig, pricingRemoteClient)
|
pricingService, err := service.ProvidePricingService(configConfig, pricingRemoteClient)
|
||||||
@@ -132,7 +134,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, apiKeyService, subscriptionService)
|
engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, apiKeyService, subscriptionService)
|
||||||
httpServer := server.ProvideHTTPServer(configConfig, engine)
|
httpServer := server.ProvideHTTPServer(configConfig, engine)
|
||||||
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
|
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
|
||||||
v := provideCleanup(db, client, tokenRefreshService, pricingService, emailQueueService, oAuthService, openAIOAuthService, geminiOAuthService)
|
v := provideCleanup(db, client, tokenRefreshService, pricingService, emailQueueService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService)
|
||||||
application := &Application{
|
application := &Application{
|
||||||
Server: httpServer,
|
Server: httpServer,
|
||||||
Cleanup: v,
|
Cleanup: v,
|
||||||
@@ -163,6 +165,7 @@ func provideCleanup(
|
|||||||
oauth *service.OAuthService,
|
oauth *service.OAuthService,
|
||||||
openaiOAuth *service.OpenAIOAuthService,
|
openaiOAuth *service.OpenAIOAuthService,
|
||||||
geminiOAuth *service.GeminiOAuthService,
|
geminiOAuth *service.GeminiOAuthService,
|
||||||
|
antigravityOAuth *service.AntigravityOAuthService,
|
||||||
) func() {
|
) func() {
|
||||||
return func() {
|
return func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
@@ -196,6 +199,10 @@ func provideCleanup(
|
|||||||
geminiOAuth.Stop()
|
geminiOAuth.Stop()
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"AntigravityOAuthService", func() error {
|
||||||
|
antigravityOAuth.Stop()
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"Redis", func() error {
|
{"Redis", func() error {
|
||||||
return rdb.Close()
|
return rdb.Close()
|
||||||
}},
|
}},
|
||||||
|
|||||||
67
backend/internal/handler/admin/antigravity_oauth_handler.go
Normal file
67
backend/internal/handler/admin/antigravity_oauth_handler.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AntigravityOAuthHandler struct {
|
||||||
|
antigravityOAuthService *service.AntigravityOAuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAntigravityOAuthHandler(antigravityOAuthService *service.AntigravityOAuthService) *AntigravityOAuthHandler {
|
||||||
|
return &AntigravityOAuthHandler{antigravityOAuthService: antigravityOAuthService}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AntigravityGenerateAuthURLRequest struct {
|
||||||
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAuthURL generates Google OAuth authorization URL
|
||||||
|
// POST /api/v1/admin/antigravity/oauth/auth-url
|
||||||
|
func (h *AntigravityOAuthHandler) GenerateAuthURL(c *gin.Context) {
|
||||||
|
var req AntigravityGenerateAuthURLRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "请求无效: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.antigravityOAuthService.GenerateAuthURL(c.Request.Context(), req.ProxyID)
|
||||||
|
if err != nil {
|
||||||
|
response.InternalError(c, "生成授权链接失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AntigravityExchangeCodeRequest struct {
|
||||||
|
SessionID string `json:"session_id" binding:"required"`
|
||||||
|
State string `json:"state" binding:"required"`
|
||||||
|
Code string `json:"code" binding:"required"`
|
||||||
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeCode 用 authorization code 交换 token
|
||||||
|
// POST /api/v1/admin/antigravity/oauth/exchange-code
|
||||||
|
func (h *AntigravityOAuthHandler) ExchangeCode(c *gin.Context) {
|
||||||
|
var req AntigravityExchangeCodeRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "请求无效: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenInfo, err := h.antigravityOAuthService.ExchangeCode(c.Request.Context(), &service.AntigravityExchangeCodeInput{
|
||||||
|
SessionID: req.SessionID,
|
||||||
|
State: req.State,
|
||||||
|
Code: req.Code,
|
||||||
|
ProxyID: req.ProxyID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, "Token 交换失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, tokenInfo)
|
||||||
|
}
|
||||||
@@ -6,19 +6,20 @@ import (
|
|||||||
|
|
||||||
// AdminHandlers contains all admin-related HTTP handlers
|
// AdminHandlers contains all admin-related HTTP handlers
|
||||||
type AdminHandlers struct {
|
type AdminHandlers struct {
|
||||||
Dashboard *admin.DashboardHandler
|
Dashboard *admin.DashboardHandler
|
||||||
User *admin.UserHandler
|
User *admin.UserHandler
|
||||||
Group *admin.GroupHandler
|
Group *admin.GroupHandler
|
||||||
Account *admin.AccountHandler
|
Account *admin.AccountHandler
|
||||||
OAuth *admin.OAuthHandler
|
OAuth *admin.OAuthHandler
|
||||||
OpenAIOAuth *admin.OpenAIOAuthHandler
|
OpenAIOAuth *admin.OpenAIOAuthHandler
|
||||||
GeminiOAuth *admin.GeminiOAuthHandler
|
GeminiOAuth *admin.GeminiOAuthHandler
|
||||||
Proxy *admin.ProxyHandler
|
AntigravityOAuth *admin.AntigravityOAuthHandler
|
||||||
Redeem *admin.RedeemHandler
|
Proxy *admin.ProxyHandler
|
||||||
Setting *admin.SettingHandler
|
Redeem *admin.RedeemHandler
|
||||||
System *admin.SystemHandler
|
Setting *admin.SettingHandler
|
||||||
Subscription *admin.SubscriptionHandler
|
System *admin.SystemHandler
|
||||||
Usage *admin.UsageHandler
|
Subscription *admin.SubscriptionHandler
|
||||||
|
Usage *admin.UsageHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handlers contains all HTTP handlers
|
// Handlers contains all HTTP handlers
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ func ProvideAdminHandlers(
|
|||||||
oauthHandler *admin.OAuthHandler,
|
oauthHandler *admin.OAuthHandler,
|
||||||
openaiOAuthHandler *admin.OpenAIOAuthHandler,
|
openaiOAuthHandler *admin.OpenAIOAuthHandler,
|
||||||
geminiOAuthHandler *admin.GeminiOAuthHandler,
|
geminiOAuthHandler *admin.GeminiOAuthHandler,
|
||||||
|
antigravityOAuthHandler *admin.AntigravityOAuthHandler,
|
||||||
proxyHandler *admin.ProxyHandler,
|
proxyHandler *admin.ProxyHandler,
|
||||||
redeemHandler *admin.RedeemHandler,
|
redeemHandler *admin.RedeemHandler,
|
||||||
settingHandler *admin.SettingHandler,
|
settingHandler *admin.SettingHandler,
|
||||||
@@ -24,19 +25,20 @@ func ProvideAdminHandlers(
|
|||||||
usageHandler *admin.UsageHandler,
|
usageHandler *admin.UsageHandler,
|
||||||
) *AdminHandlers {
|
) *AdminHandlers {
|
||||||
return &AdminHandlers{
|
return &AdminHandlers{
|
||||||
Dashboard: dashboardHandler,
|
Dashboard: dashboardHandler,
|
||||||
User: userHandler,
|
User: userHandler,
|
||||||
Group: groupHandler,
|
Group: groupHandler,
|
||||||
Account: accountHandler,
|
Account: accountHandler,
|
||||||
OAuth: oauthHandler,
|
OAuth: oauthHandler,
|
||||||
OpenAIOAuth: openaiOAuthHandler,
|
OpenAIOAuth: openaiOAuthHandler,
|
||||||
GeminiOAuth: geminiOAuthHandler,
|
GeminiOAuth: geminiOAuthHandler,
|
||||||
Proxy: proxyHandler,
|
AntigravityOAuth: antigravityOAuthHandler,
|
||||||
Redeem: redeemHandler,
|
Proxy: proxyHandler,
|
||||||
Setting: settingHandler,
|
Redeem: redeemHandler,
|
||||||
System: systemHandler,
|
Setting: settingHandler,
|
||||||
Subscription: subscriptionHandler,
|
System: systemHandler,
|
||||||
Usage: usageHandler,
|
Subscription: subscriptionHandler,
|
||||||
|
Usage: usageHandler,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +100,7 @@ var ProviderSet = wire.NewSet(
|
|||||||
admin.NewOAuthHandler,
|
admin.NewOAuthHandler,
|
||||||
admin.NewOpenAIOAuthHandler,
|
admin.NewOpenAIOAuthHandler,
|
||||||
admin.NewGeminiOAuthHandler,
|
admin.NewGeminiOAuthHandler,
|
||||||
|
admin.NewAntigravityOAuthHandler,
|
||||||
admin.NewProxyHandler,
|
admin.NewProxyHandler,
|
||||||
admin.NewRedeemHandler,
|
admin.NewRedeemHandler,
|
||||||
admin.NewSettingHandler,
|
admin.NewSettingHandler,
|
||||||
|
|||||||
216
backend/internal/pkg/antigravity/client.go
Normal file
216
backend/internal/pkg/antigravity/client.go
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
package antigravity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenResponse Google OAuth token 响应
|
||||||
|
type TokenResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int64 `json:"expires_in"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
Scope string `json:"scope,omitempty"`
|
||||||
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInfo Google 用户信息
|
||||||
|
type UserInfo struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
GivenName string `json:"given_name,omitempty"`
|
||||||
|
FamilyName string `json:"family_name,omitempty"`
|
||||||
|
Picture string `json:"picture,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCodeAssistRequest loadCodeAssist 请求
|
||||||
|
type LoadCodeAssistRequest struct {
|
||||||
|
Metadata struct {
|
||||||
|
IDEType string `json:"ideType"`
|
||||||
|
} `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCodeAssistResponse loadCodeAssist 响应
|
||||||
|
type LoadCodeAssistResponse struct {
|
||||||
|
CloudAICompanionProject string `json:"cloudaicompanionProject"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client Antigravity API 客户端
|
||||||
|
type Client struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
proxyURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(proxyURL string) *Client {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(proxyURL) != "" {
|
||||||
|
if proxyURLParsed, err := url.Parse(proxyURL); err == nil {
|
||||||
|
client.Transport = &http.Transport{
|
||||||
|
Proxy: http.ProxyURL(proxyURLParsed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
httpClient: client,
|
||||||
|
proxyURL: proxyURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeCode 用 authorization code 交换 token
|
||||||
|
func (c *Client) ExchangeCode(ctx context.Context, code, codeVerifier string) (*TokenResponse, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("client_id", ClientID)
|
||||||
|
params.Set("client_secret", ClientSecret)
|
||||||
|
params.Set("code", code)
|
||||||
|
params.Set("redirect_uri", RedirectURI)
|
||||||
|
params.Set("grant_type", "authorization_code")
|
||||||
|
params.Set("code_verifier", codeVerifier)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, TokenURL, strings.NewReader(params.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Token 交换请求失败: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("Token 交换失败 (HTTP %d): %s", resp.StatusCode, string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenResp TokenResponse
|
||||||
|
if err := json.Unmarshal(bodyBytes, &tokenResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("Token 解析失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tokenResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken 刷新 access_token
|
||||||
|
func (c *Client) RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("client_id", ClientID)
|
||||||
|
params.Set("client_secret", ClientSecret)
|
||||||
|
params.Set("refresh_token", refreshToken)
|
||||||
|
params.Set("grant_type", "refresh_token")
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, TokenURL, strings.NewReader(params.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Token 刷新请求失败: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("Token 刷新失败 (HTTP %d): %s", resp.StatusCode, string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenResp TokenResponse
|
||||||
|
if err := json.Unmarshal(bodyBytes, &tokenResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("Token 解析失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tokenResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserInfo 获取用户信息
|
||||||
|
func (c *Client) GetUserInfo(ctx context.Context, accessToken string) (*UserInfo, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, UserInfoURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("用户信息请求失败: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("获取用户信息失败 (HTTP %d): %s", resp.StatusCode, string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
var userInfo UserInfo
|
||||||
|
if err := json.Unmarshal(bodyBytes, &userInfo); err != nil {
|
||||||
|
return nil, fmt.Errorf("用户信息解析失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &userInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCodeAssist 获取 project_id
|
||||||
|
func (c *Client) LoadCodeAssist(ctx context.Context, accessToken string) (*LoadCodeAssistResponse, error) {
|
||||||
|
reqBody := LoadCodeAssistRequest{}
|
||||||
|
reqBody.Metadata.IDEType = "ANTIGRAVITY"
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("序列化请求失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := BaseURL + "/v1internal:loadCodeAssist"
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(bodyBytes)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("User-Agent", UserAgent)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("loadCodeAssist 请求失败: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
respBodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("loadCodeAssist 失败 (HTTP %d): %s", resp.StatusCode, string(respBodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadResp LoadCodeAssistResponse
|
||||||
|
if err := json.Unmarshal(respBodyBytes, &loadResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("响应解析失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &loadResp, nil
|
||||||
|
}
|
||||||
179
backend/internal/pkg/antigravity/oauth.go
Normal file
179
backend/internal/pkg/antigravity/oauth.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package antigravity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Google OAuth 端点
|
||||||
|
AuthorizeURL = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||||
|
TokenURL = "https://oauth2.googleapis.com/token"
|
||||||
|
UserInfoURL = "https://www.googleapis.com/oauth2/v2/userinfo"
|
||||||
|
|
||||||
|
// Antigravity OAuth 客户端凭证
|
||||||
|
ClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
|
||||||
|
ClientSecret = "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"
|
||||||
|
|
||||||
|
// 固定的 redirect_uri(用户需手动复制 code)
|
||||||
|
RedirectURI = "http://localhost:8085/callback"
|
||||||
|
|
||||||
|
// OAuth scopes
|
||||||
|
Scopes = "https://www.googleapis.com/auth/cloud-platform " +
|
||||||
|
"https://www.googleapis.com/auth/userinfo.email " +
|
||||||
|
"https://www.googleapis.com/auth/userinfo.profile " +
|
||||||
|
"https://www.googleapis.com/auth/cclog " +
|
||||||
|
"https://www.googleapis.com/auth/experimentsandconfigs"
|
||||||
|
|
||||||
|
// API 端点
|
||||||
|
BaseURL = "https://cloudcode-pa.googleapis.com"
|
||||||
|
|
||||||
|
// User-Agent
|
||||||
|
UserAgent = "antigravity/1.11.9 windows/amd64"
|
||||||
|
|
||||||
|
// Session 过期时间
|
||||||
|
SessionTTL = 30 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuthSession 保存 OAuth 授权流程的临时状态
|
||||||
|
type OAuthSession struct {
|
||||||
|
State string `json:"state"`
|
||||||
|
CodeVerifier string `json:"code_verifier"`
|
||||||
|
ProxyURL string `json:"proxy_url,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionStore OAuth session 存储
|
||||||
|
type SessionStore struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
sessions map[string]*OAuthSession
|
||||||
|
stopCh chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionStore() *SessionStore {
|
||||||
|
store := &SessionStore{
|
||||||
|
sessions: make(map[string]*OAuthSession),
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
go store.cleanup()
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionStore) Set(sessionID string, session *OAuthSession) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.sessions[sessionID] = session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionStore) Get(sessionID string) (*OAuthSession, bool) {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
session, ok := s.sessions[sessionID]
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if time.Since(session.CreatedAt) > SessionTTL {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return session, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionStore) Delete(sessionID string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
delete(s.sessions, sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionStore) Stop() {
|
||||||
|
select {
|
||||||
|
case <-s.stopCh:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
close(s.stopCh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionStore) cleanup() {
|
||||||
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.stopCh:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
s.mu.Lock()
|
||||||
|
for id, session := range s.sessions {
|
||||||
|
if time.Since(session.CreatedAt) > SessionTTL {
|
||||||
|
delete(s.sessions, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateRandomBytes(n int) ([]byte, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateState() (string, error) {
|
||||||
|
bytes, err := GenerateRandomBytes(32)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64URLEncode(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateSessionID() (string, error) {
|
||||||
|
bytes, err := GenerateRandomBytes(16)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateCodeVerifier() (string, error) {
|
||||||
|
bytes, err := GenerateRandomBytes(32)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64URLEncode(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateCodeChallenge(verifier string) string {
|
||||||
|
hash := sha256.Sum256([]byte(verifier))
|
||||||
|
return base64URLEncode(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func base64URLEncode(data []byte) string {
|
||||||
|
return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=")
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildAuthorizationURL 构建 Google OAuth 授权 URL
|
||||||
|
func BuildAuthorizationURL(state, codeChallenge string) string {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("client_id", ClientID)
|
||||||
|
params.Set("redirect_uri", RedirectURI)
|
||||||
|
params.Set("response_type", "code")
|
||||||
|
params.Set("scope", Scopes)
|
||||||
|
params.Set("state", state)
|
||||||
|
params.Set("code_challenge", codeChallenge)
|
||||||
|
params.Set("code_challenge_method", "S256")
|
||||||
|
params.Set("access_type", "offline")
|
||||||
|
params.Set("prompt", "consent")
|
||||||
|
params.Set("include_granted_scopes", "true")
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s?%s", AuthorizeURL, params.Encode())
|
||||||
|
}
|
||||||
@@ -148,6 +148,14 @@ func registerGeminiOAuthRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerAntigravityOAuthRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
||||||
|
antigravity := admin.Group("/antigravity")
|
||||||
|
{
|
||||||
|
antigravity.POST("/oauth/auth-url", h.Admin.AntigravityOAuth.GenerateAuthURL)
|
||||||
|
antigravity.POST("/oauth/exchange-code", h.Admin.AntigravityOAuth.ExchangeCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func registerProxyRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
func registerProxyRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
||||||
proxies := admin.Group("/proxies")
|
proxies := admin.Group("/proxies")
|
||||||
{
|
{
|
||||||
|
|||||||
267
backend/internal/service/antigravity_oauth_service.go
Normal file
267
backend/internal/service/antigravity_oauth_service.go
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AntigravityOAuthService struct {
|
||||||
|
sessionStore *antigravity.SessionStore
|
||||||
|
proxyRepo ProxyRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAntigravityOAuthService(proxyRepo ProxyRepository) *AntigravityOAuthService {
|
||||||
|
return &AntigravityOAuthService{
|
||||||
|
sessionStore: antigravity.NewSessionStore(),
|
||||||
|
proxyRepo: proxyRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AntigravityAuthURLResult is the result of generating an authorization URL
|
||||||
|
type AntigravityAuthURLResult struct {
|
||||||
|
AuthURL string `json:"auth_url"`
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
State string `json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAuthURL 生成 Google OAuth 授权链接
|
||||||
|
func (s *AntigravityOAuthService) GenerateAuthURL(ctx context.Context, proxyID *int64) (*AntigravityAuthURLResult, error) {
|
||||||
|
state, err := antigravity.GenerateState()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("生成 state 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
codeVerifier, err := antigravity.GenerateCodeVerifier()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("生成 code_verifier 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionID, err := antigravity.GenerateSessionID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("生成 session_id 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxyURL string
|
||||||
|
if proxyID != nil {
|
||||||
|
proxy, err := s.proxyRepo.GetByID(ctx, *proxyID)
|
||||||
|
if err == nil && proxy != nil {
|
||||||
|
proxyURL = proxy.URL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session := &antigravity.OAuthSession{
|
||||||
|
State: state,
|
||||||
|
CodeVerifier: codeVerifier,
|
||||||
|
ProxyURL: proxyURL,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
s.sessionStore.Set(sessionID, session)
|
||||||
|
|
||||||
|
codeChallenge := antigravity.GenerateCodeChallenge(codeVerifier)
|
||||||
|
authURL := antigravity.BuildAuthorizationURL(state, codeChallenge)
|
||||||
|
|
||||||
|
return &AntigravityAuthURLResult{
|
||||||
|
AuthURL: authURL,
|
||||||
|
SessionID: sessionID,
|
||||||
|
State: state,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AntigravityExchangeCodeInput 交换 code 的输入
|
||||||
|
type AntigravityExchangeCodeInput struct {
|
||||||
|
SessionID string
|
||||||
|
State string
|
||||||
|
Code string
|
||||||
|
ProxyID *int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// AntigravityTokenInfo token 信息
|
||||||
|
type AntigravityTokenInfo struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
ExpiresIn int64 `json:"expires_in"`
|
||||||
|
ExpiresAt int64 `json:"expires_at"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
ProjectID string `json:"project_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeCode 用 authorization code 交换 token
|
||||||
|
func (s *AntigravityOAuthService) ExchangeCode(ctx context.Context, input *AntigravityExchangeCodeInput) (*AntigravityTokenInfo, error) {
|
||||||
|
session, ok := s.sessionStore.Get(input.SessionID)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("session 不存在或已过期")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(input.State) == "" || input.State != session.State {
|
||||||
|
return nil, fmt.Errorf("state 无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定代理 URL
|
||||||
|
proxyURL := session.ProxyURL
|
||||||
|
if input.ProxyID != nil {
|
||||||
|
proxy, err := s.proxyRepo.GetByID(ctx, *input.ProxyID)
|
||||||
|
if err == nil && proxy != nil {
|
||||||
|
proxyURL = proxy.URL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client := antigravity.NewClient(proxyURL)
|
||||||
|
|
||||||
|
// 交换 token
|
||||||
|
tokenResp, err := client.ExchangeCode(ctx, input.Code, session.CodeVerifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Token 交换失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除 session
|
||||||
|
s.sessionStore.Delete(input.SessionID)
|
||||||
|
|
||||||
|
// 计算过期时间(减去 5 分钟安全窗口)
|
||||||
|
expiresAt := time.Now().Unix() + tokenResp.ExpiresIn - 300
|
||||||
|
|
||||||
|
result := &AntigravityTokenInfo{
|
||||||
|
AccessToken: tokenResp.AccessToken,
|
||||||
|
RefreshToken: tokenResp.RefreshToken,
|
||||||
|
ExpiresIn: tokenResp.ExpiresIn,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
TokenType: tokenResp.TokenType,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
userInfo, err := client.GetUserInfo(ctx, tokenResp.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[AntigravityOAuth] 警告: 获取用户信息失败: %v\n", err)
|
||||||
|
} else {
|
||||||
|
result.Email = userInfo.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 project_id
|
||||||
|
loadResp, err := client.LoadCodeAssist(ctx, tokenResp.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[AntigravityOAuth] 警告: 获取 project_id 失败: %v\n", err)
|
||||||
|
} else if loadResp != nil && loadResp.CloudAICompanionProject != "" {
|
||||||
|
result.ProjectID = loadResp.CloudAICompanionProject
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken 刷新 token
|
||||||
|
func (s *AntigravityOAuthService) RefreshToken(ctx context.Context, refreshToken, proxyURL string) (*AntigravityTokenInfo, error) {
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
for attempt := 0; attempt <= 3; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
backoff := time.Duration(1<<uint(attempt-1)) * time.Second
|
||||||
|
if backoff > 30*time.Second {
|
||||||
|
backoff = 30 * time.Second
|
||||||
|
}
|
||||||
|
time.Sleep(backoff)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := antigravity.NewClient(proxyURL)
|
||||||
|
tokenResp, err := client.RefreshToken(ctx, refreshToken)
|
||||||
|
if err == nil {
|
||||||
|
expiresAt := time.Now().Unix() + tokenResp.ExpiresIn - 300
|
||||||
|
return &AntigravityTokenInfo{
|
||||||
|
AccessToken: tokenResp.AccessToken,
|
||||||
|
RefreshToken: tokenResp.RefreshToken,
|
||||||
|
ExpiresIn: tokenResp.ExpiresIn,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
TokenType: tokenResp.TokenType,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isNonRetryableAntigravityOAuthError(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Token 刷新失败 (重试后): %w", lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNonRetryableAntigravityOAuthError(err error) bool {
|
||||||
|
msg := err.Error()
|
||||||
|
nonRetryable := []string{
|
||||||
|
"invalid_grant",
|
||||||
|
"invalid_client",
|
||||||
|
"unauthorized_client",
|
||||||
|
"access_denied",
|
||||||
|
}
|
||||||
|
for _, needle := range nonRetryable {
|
||||||
|
if strings.Contains(msg, needle) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshAccountToken 刷新账户的 token
|
||||||
|
func (s *AntigravityOAuthService) RefreshAccountToken(ctx context.Context, account *Account) (*AntigravityTokenInfo, error) {
|
||||||
|
if account.Platform != PlatformAntigravity || account.Type != AccountTypeOAuth {
|
||||||
|
return nil, fmt.Errorf("非 Antigravity OAuth 账户")
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken := account.GetCredential("refresh_token")
|
||||||
|
if strings.TrimSpace(refreshToken) == "" {
|
||||||
|
return nil, fmt.Errorf("无可用的 refresh_token")
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxyURL string
|
||||||
|
if account.ProxyID != nil {
|
||||||
|
proxy, err := s.proxyRepo.GetByID(ctx, *account.ProxyID)
|
||||||
|
if err == nil && proxy != nil {
|
||||||
|
proxyURL = proxy.URL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenInfo, err := s.RefreshToken(ctx, refreshToken, proxyURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留原有的 project_id 和 email
|
||||||
|
existingProjectID := strings.TrimSpace(account.GetCredential("project_id"))
|
||||||
|
if existingProjectID != "" {
|
||||||
|
tokenInfo.ProjectID = existingProjectID
|
||||||
|
}
|
||||||
|
existingEmail := strings.TrimSpace(account.GetCredential("email"))
|
||||||
|
if existingEmail != "" {
|
||||||
|
tokenInfo.Email = existingEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildAccountCredentials 构建账户凭证
|
||||||
|
func (s *AntigravityOAuthService) BuildAccountCredentials(tokenInfo *AntigravityTokenInfo) map[string]any {
|
||||||
|
creds := map[string]any{
|
||||||
|
"access_token": tokenInfo.AccessToken,
|
||||||
|
"expires_at": strconv.FormatInt(tokenInfo.ExpiresAt, 10),
|
||||||
|
}
|
||||||
|
if tokenInfo.RefreshToken != "" {
|
||||||
|
creds["refresh_token"] = tokenInfo.RefreshToken
|
||||||
|
}
|
||||||
|
if tokenInfo.TokenType != "" {
|
||||||
|
creds["token_type"] = tokenInfo.TokenType
|
||||||
|
}
|
||||||
|
if tokenInfo.Email != "" {
|
||||||
|
creds["email"] = tokenInfo.Email
|
||||||
|
}
|
||||||
|
if tokenInfo.ProjectID != "" {
|
||||||
|
creds["project_id"] = tokenInfo.ProjectID
|
||||||
|
}
|
||||||
|
return creds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 停止服务
|
||||||
|
func (s *AntigravityOAuthService) Stop() {
|
||||||
|
s.sessionStore.Stop()
|
||||||
|
}
|
||||||
@@ -18,9 +18,10 @@ const (
|
|||||||
|
|
||||||
// Platform constants
|
// Platform constants
|
||||||
const (
|
const (
|
||||||
PlatformAnthropic = "anthropic"
|
PlatformAnthropic = "anthropic"
|
||||||
PlatformOpenAI = "openai"
|
PlatformOpenAI = "openai"
|
||||||
PlatformGemini = "gemini"
|
PlatformGemini = "gemini"
|
||||||
|
PlatformAntigravity = "antigravity"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Account type constants
|
// Account type constants
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type BuildInfo struct {
|
|||||||
func ProvidePricingService(cfg *config.Config, remoteClient PricingRemoteClient) (*PricingService, error) {
|
func ProvidePricingService(cfg *config.Config, remoteClient PricingRemoteClient) (*PricingService, error) {
|
||||||
svc := NewPricingService(cfg, remoteClient)
|
svc := NewPricingService(cfg, remoteClient)
|
||||||
if err := svc.Initialize(); err != nil {
|
if err := svc.Initialize(); err != nil {
|
||||||
// 价格服务初始化失败不应阻止启动,使用回退价格
|
// Pricing service initialization failure should not block startup, use fallback prices
|
||||||
println("[Service] Warning: Pricing service initialization failed:", err.Error())
|
println("[Service] Warning: Pricing service initialization failed:", err.Error())
|
||||||
}
|
}
|
||||||
return svc, nil
|
return svc, nil
|
||||||
@@ -81,6 +81,7 @@ var ProviderSet = wire.NewSet(
|
|||||||
NewOAuthService,
|
NewOAuthService,
|
||||||
NewOpenAIOAuthService,
|
NewOpenAIOAuthService,
|
||||||
NewGeminiOAuthService,
|
NewGeminiOAuthService,
|
||||||
|
NewAntigravityOAuthService,
|
||||||
NewGeminiTokenProvider,
|
NewGeminiTokenProvider,
|
||||||
NewGeminiMessagesCompatService,
|
NewGeminiMessagesCompatService,
|
||||||
NewRateLimitService,
|
NewRateLimitService,
|
||||||
|
|||||||
56
frontend/src/api/admin/antigravity.ts
Normal file
56
frontend/src/api/admin/antigravity.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Admin Antigravity API endpoints
|
||||||
|
* Handles Antigravity (Google Cloud AI Companion) OAuth flows for administrators
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiClient } from '../client'
|
||||||
|
|
||||||
|
export interface AntigravityAuthUrlResponse {
|
||||||
|
auth_url: string
|
||||||
|
session_id: string
|
||||||
|
state: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AntigravityAuthUrlRequest {
|
||||||
|
proxy_id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AntigravityExchangeCodeRequest {
|
||||||
|
session_id: string
|
||||||
|
state: string
|
||||||
|
code: string
|
||||||
|
proxy_id?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AntigravityTokenInfo {
|
||||||
|
access_token?: string
|
||||||
|
refresh_token?: string
|
||||||
|
token_type?: string
|
||||||
|
expires_at?: number | string
|
||||||
|
expires_in?: number
|
||||||
|
project_id?: string
|
||||||
|
email?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateAuthUrl(
|
||||||
|
payload: AntigravityAuthUrlRequest
|
||||||
|
): Promise<AntigravityAuthUrlResponse> {
|
||||||
|
const { data } = await apiClient.post<AntigravityAuthUrlResponse>(
|
||||||
|
'/admin/antigravity/oauth/auth-url',
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exchangeCode(
|
||||||
|
payload: AntigravityExchangeCodeRequest
|
||||||
|
): Promise<AntigravityTokenInfo> {
|
||||||
|
const { data } = await apiClient.post<AntigravityTokenInfo>(
|
||||||
|
'/admin/antigravity/oauth/exchange-code',
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { generateAuthUrl, exchangeCode }
|
||||||
@@ -14,6 +14,7 @@ import systemAPI from './system'
|
|||||||
import subscriptionsAPI from './subscriptions'
|
import subscriptionsAPI from './subscriptions'
|
||||||
import usageAPI from './usage'
|
import usageAPI from './usage'
|
||||||
import geminiAPI from './gemini'
|
import geminiAPI from './gemini'
|
||||||
|
import antigravityAPI from './antigravity'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unified admin API object for convenient access
|
* Unified admin API object for convenient access
|
||||||
@@ -29,7 +30,8 @@ export const adminAPI = {
|
|||||||
system: systemAPI,
|
system: systemAPI,
|
||||||
subscriptions: subscriptionsAPI,
|
subscriptions: subscriptionsAPI,
|
||||||
usage: usageAPI,
|
usage: usageAPI,
|
||||||
gemini: geminiAPI
|
gemini: geminiAPI,
|
||||||
|
antigravity: antigravityAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -43,7 +45,8 @@ export {
|
|||||||
systemAPI,
|
systemAPI,
|
||||||
subscriptionsAPI,
|
subscriptionsAPI,
|
||||||
usageAPI,
|
usageAPI,
|
||||||
geminiAPI
|
geminiAPI,
|
||||||
|
antigravityAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
export default adminAPI
|
export default adminAPI
|
||||||
|
|||||||
@@ -125,6 +125,31 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Gemini
|
Gemini
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="form.platform = 'antigravity'"
|
||||||
|
:class="[
|
||||||
|
'flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all',
|
||||||
|
form.platform === 'antigravity'
|
||||||
|
? 'bg-white text-purple-600 shadow-sm dark:bg-dark-600 dark:text-purple-400'
|
||||||
|
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-4 w-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Antigravity
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -477,6 +502,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Account Type Selection (Antigravity - OAuth only) -->
|
||||||
|
<div v-if="form.platform === 'antigravity'">
|
||||||
|
<label class="input-label">{{ t('admin.accounts.accountType') }}</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 rounded-lg border-2 border-purple-500 bg-purple-50 p-3 dark:bg-purple-900/20"
|
||||||
|
>
|
||||||
|
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-purple-500 text-white">
|
||||||
|
<svg
|
||||||
|
class="h-4 w-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="block text-sm font-medium text-gray-900 dark:text-white">OAuth</span>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.types.antigravityOauth') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Add Method (only for Anthropic OAuth-based type) -->
|
<!-- Add Method (only for Anthropic OAuth-based type) -->
|
||||||
<div v-if="form.platform === 'anthropic' && isOAuthFlow">
|
<div v-if="form.platform === 'anthropic' && isOAuthFlow">
|
||||||
<label class="input-label">{{ t('admin.accounts.addMethod') }}</label>
|
<label class="input-label">{{ t('admin.accounts.addMethod') }}</label>
|
||||||
@@ -1072,6 +1127,7 @@ import {
|
|||||||
} from '@/composables/useAccountOAuth'
|
} from '@/composables/useAccountOAuth'
|
||||||
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
||||||
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
||||||
|
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
|
||||||
import type { Proxy, Group, AccountPlatform, AccountType } from '@/types'
|
import type { Proxy, Group, AccountPlatform, AccountType } from '@/types'
|
||||||
import Modal from '@/components/common/Modal.vue'
|
import Modal from '@/components/common/Modal.vue'
|
||||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||||
@@ -1094,6 +1150,7 @@ const { t } = useI18n()
|
|||||||
const oauthStepTitle = computed(() => {
|
const oauthStepTitle = computed(() => {
|
||||||
if (form.platform === 'openai') return t('admin.accounts.oauth.openai.title')
|
if (form.platform === 'openai') return t('admin.accounts.oauth.openai.title')
|
||||||
if (form.platform === 'gemini') return t('admin.accounts.oauth.gemini.title')
|
if (form.platform === 'gemini') return t('admin.accounts.oauth.gemini.title')
|
||||||
|
if (form.platform === 'antigravity') return t('admin.accounts.oauth.antigravity.title')
|
||||||
return t('admin.accounts.oauth.title')
|
return t('admin.accounts.oauth.title')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1115,29 +1172,34 @@ const appStore = useAppStore()
|
|||||||
const oauth = useAccountOAuth() // For Anthropic OAuth
|
const oauth = useAccountOAuth() // For Anthropic OAuth
|
||||||
const openaiOAuth = useOpenAIOAuth() // For OpenAI OAuth
|
const openaiOAuth = useOpenAIOAuth() // For OpenAI OAuth
|
||||||
const geminiOAuth = useGeminiOAuth() // For Gemini OAuth
|
const geminiOAuth = useGeminiOAuth() // For Gemini OAuth
|
||||||
|
const antigravityOAuth = useAntigravityOAuth() // For Antigravity OAuth
|
||||||
|
|
||||||
// Computed: current OAuth state for template binding
|
// Computed: current OAuth state for template binding
|
||||||
const currentAuthUrl = computed(() => {
|
const currentAuthUrl = computed(() => {
|
||||||
if (form.platform === 'openai') return openaiOAuth.authUrl.value
|
if (form.platform === 'openai') return openaiOAuth.authUrl.value
|
||||||
if (form.platform === 'gemini') return geminiOAuth.authUrl.value
|
if (form.platform === 'gemini') return geminiOAuth.authUrl.value
|
||||||
|
if (form.platform === 'antigravity') return antigravityOAuth.authUrl.value
|
||||||
return oauth.authUrl.value
|
return oauth.authUrl.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentSessionId = computed(() => {
|
const currentSessionId = computed(() => {
|
||||||
if (form.platform === 'openai') return openaiOAuth.sessionId.value
|
if (form.platform === 'openai') return openaiOAuth.sessionId.value
|
||||||
if (form.platform === 'gemini') return geminiOAuth.sessionId.value
|
if (form.platform === 'gemini') return geminiOAuth.sessionId.value
|
||||||
|
if (form.platform === 'antigravity') return antigravityOAuth.sessionId.value
|
||||||
return oauth.sessionId.value
|
return oauth.sessionId.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentOAuthLoading = computed(() => {
|
const currentOAuthLoading = computed(() => {
|
||||||
if (form.platform === 'openai') return openaiOAuth.loading.value
|
if (form.platform === 'openai') return openaiOAuth.loading.value
|
||||||
if (form.platform === 'gemini') return geminiOAuth.loading.value
|
if (form.platform === 'gemini') return geminiOAuth.loading.value
|
||||||
|
if (form.platform === 'antigravity') return antigravityOAuth.loading.value
|
||||||
return oauth.loading.value
|
return oauth.loading.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentOAuthError = computed(() => {
|
const currentOAuthError = computed(() => {
|
||||||
if (form.platform === 'openai') return openaiOAuth.error.value
|
if (form.platform === 'openai') return openaiOAuth.error.value
|
||||||
if (form.platform === 'gemini') return geminiOAuth.error.value
|
if (form.platform === 'gemini') return geminiOAuth.error.value
|
||||||
|
if (form.platform === 'antigravity') return antigravityOAuth.error.value
|
||||||
return oauth.error.value
|
return oauth.error.value
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1366,6 +1428,9 @@ const canExchangeCode = computed(() => {
|
|||||||
if (form.platform === 'gemini') {
|
if (form.platform === 'gemini') {
|
||||||
return authCode.trim() && geminiOAuth.sessionId.value && !geminiOAuth.loading.value
|
return authCode.trim() && geminiOAuth.sessionId.value && !geminiOAuth.loading.value
|
||||||
}
|
}
|
||||||
|
if (form.platform === 'antigravity') {
|
||||||
|
return authCode.trim() && antigravityOAuth.sessionId.value && !antigravityOAuth.loading.value
|
||||||
|
}
|
||||||
return authCode.trim() && oauth.sessionId.value && !oauth.loading.value
|
return authCode.trim() && oauth.sessionId.value && !oauth.loading.value
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1410,10 +1475,15 @@ watch(
|
|||||||
if (newPlatform !== 'anthropic') {
|
if (newPlatform !== 'anthropic') {
|
||||||
interceptWarmupRequests.value = false
|
interceptWarmupRequests.value = false
|
||||||
}
|
}
|
||||||
|
// Antigravity only supports OAuth
|
||||||
|
if (newPlatform === 'antigravity') {
|
||||||
|
accountCategory.value = 'oauth-based'
|
||||||
|
}
|
||||||
// Reset OAuth states
|
// Reset OAuth states
|
||||||
oauth.resetState()
|
oauth.resetState()
|
||||||
openaiOAuth.resetState()
|
openaiOAuth.resetState()
|
||||||
geminiOAuth.resetState()
|
geminiOAuth.resetState()
|
||||||
|
antigravityOAuth.resetState()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1542,6 +1612,7 @@ const resetForm = () => {
|
|||||||
oauth.resetState()
|
oauth.resetState()
|
||||||
openaiOAuth.resetState()
|
openaiOAuth.resetState()
|
||||||
geminiOAuth.resetState()
|
geminiOAuth.resetState()
|
||||||
|
antigravityOAuth.resetState()
|
||||||
oauthFlowRef.value?.reset()
|
oauthFlowRef.value?.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1620,6 +1691,7 @@ const goBackToBasicInfo = () => {
|
|||||||
oauth.resetState()
|
oauth.resetState()
|
||||||
openaiOAuth.resetState()
|
openaiOAuth.resetState()
|
||||||
geminiOAuth.resetState()
|
geminiOAuth.resetState()
|
||||||
|
antigravityOAuth.resetState()
|
||||||
oauthFlowRef.value?.reset()
|
oauthFlowRef.value?.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1628,114 +1700,133 @@ const handleGenerateUrl = async () => {
|
|||||||
await openaiOAuth.generateAuthUrl(form.proxy_id)
|
await openaiOAuth.generateAuthUrl(form.proxy_id)
|
||||||
} else if (form.platform === 'gemini') {
|
} else if (form.platform === 'gemini') {
|
||||||
await geminiOAuth.generateAuthUrl(form.proxy_id, oauthFlowRef.value?.projectId, geminiOAuthType.value)
|
await geminiOAuth.generateAuthUrl(form.proxy_id, oauthFlowRef.value?.projectId, geminiOAuthType.value)
|
||||||
|
} else if (form.platform === 'antigravity') {
|
||||||
|
await antigravityOAuth.generateAuthUrl(form.proxy_id)
|
||||||
} else {
|
} else {
|
||||||
await oauth.generateAuthUrl(addMethod.value, form.proxy_id)
|
await oauth.generateAuthUrl(addMethod.value, form.proxy_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExchangeCode = async () => {
|
// Create account and handle success/failure
|
||||||
const authCode = oauthFlowRef.value?.authCode || ''
|
const createAccountAndFinish = async (
|
||||||
|
platform: AccountPlatform,
|
||||||
|
type: AccountType,
|
||||||
|
credentials: Record<string, unknown>,
|
||||||
|
extra?: Record<string, string>
|
||||||
|
) => {
|
||||||
|
await adminAPI.accounts.create({
|
||||||
|
name: form.name,
|
||||||
|
platform,
|
||||||
|
type,
|
||||||
|
credentials,
|
||||||
|
extra,
|
||||||
|
proxy_id: form.proxy_id,
|
||||||
|
concurrency: form.concurrency,
|
||||||
|
priority: form.priority,
|
||||||
|
group_ids: form.group_ids
|
||||||
|
})
|
||||||
|
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
||||||
|
emit('created')
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
// For OpenAI
|
// OpenAI OAuth 授权码兑换
|
||||||
if (form.platform === 'openai') {
|
const handleOpenAIExchange = async (authCode: string) => {
|
||||||
if (!authCode.trim() || !openaiOAuth.sessionId.value) return
|
if (!authCode.trim() || !openaiOAuth.sessionId.value) return
|
||||||
|
|
||||||
openaiOAuth.loading.value = true
|
openaiOAuth.loading.value = true
|
||||||
openaiOAuth.error.value = ''
|
openaiOAuth.error.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenInfo = await openaiOAuth.exchangeAuthCode(
|
const tokenInfo = await openaiOAuth.exchangeAuthCode(
|
||||||
authCode.trim(),
|
authCode.trim(),
|
||||||
openaiOAuth.sessionId.value,
|
openaiOAuth.sessionId.value,
|
||||||
form.proxy_id
|
form.proxy_id
|
||||||
)
|
)
|
||||||
|
if (!tokenInfo) return
|
||||||
|
|
||||||
if (!tokenInfo) {
|
const credentials = openaiOAuth.buildCredentials(tokenInfo)
|
||||||
return // Error already handled by composable
|
const extra = openaiOAuth.buildExtraInfo(tokenInfo)
|
||||||
}
|
await createAccountAndFinish('openai', 'oauth', credentials, extra)
|
||||||
|
} catch (error: any) {
|
||||||
const credentials = openaiOAuth.buildCredentials(tokenInfo)
|
openaiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
||||||
const extra = openaiOAuth.buildExtraInfo(tokenInfo)
|
appStore.showError(openaiOAuth.error.value)
|
||||||
|
} finally {
|
||||||
// Note: intercept_warmup_requests is Anthropic-only, not applicable to OpenAI
|
openaiOAuth.loading.value = false
|
||||||
|
|
||||||
await adminAPI.accounts.create({
|
|
||||||
name: form.name,
|
|
||||||
platform: 'openai',
|
|
||||||
type: 'oauth',
|
|
||||||
credentials,
|
|
||||||
extra,
|
|
||||||
proxy_id: form.proxy_id,
|
|
||||||
concurrency: form.concurrency,
|
|
||||||
priority: form.priority,
|
|
||||||
group_ids: form.group_ids
|
|
||||||
})
|
|
||||||
|
|
||||||
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
|
||||||
emit('created')
|
|
||||||
handleClose()
|
|
||||||
} catch (error: any) {
|
|
||||||
openaiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
|
||||||
appStore.showError(openaiOAuth.error.value)
|
|
||||||
} finally {
|
|
||||||
openaiOAuth.loading.value = false
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For Gemini
|
// Gemini OAuth 授权码兑换
|
||||||
if (form.platform === 'gemini') {
|
const handleGeminiExchange = async (authCode: string) => {
|
||||||
if (!authCode.trim() || !geminiOAuth.sessionId.value) return
|
if (!authCode.trim() || !geminiOAuth.sessionId.value) return
|
||||||
|
|
||||||
geminiOAuth.loading.value = true
|
geminiOAuth.loading.value = true
|
||||||
geminiOAuth.error.value = ''
|
geminiOAuth.error.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stateFromInput = oauthFlowRef.value?.oauthState || ''
|
const stateFromInput = oauthFlowRef.value?.oauthState || ''
|
||||||
const stateToUse = stateFromInput || geminiOAuth.state.value
|
const stateToUse = stateFromInput || geminiOAuth.state.value
|
||||||
if (!stateToUse) {
|
if (!stateToUse) {
|
||||||
geminiOAuth.error.value = t('admin.accounts.oauth.authFailed')
|
geminiOAuth.error.value = t('admin.accounts.oauth.authFailed')
|
||||||
appStore.showError(geminiOAuth.error.value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenInfo = await geminiOAuth.exchangeAuthCode({
|
|
||||||
code: authCode.trim(),
|
|
||||||
sessionId: geminiOAuth.sessionId.value,
|
|
||||||
state: stateToUse,
|
|
||||||
proxyId: form.proxy_id,
|
|
||||||
oauthType: geminiOAuthType.value
|
|
||||||
})
|
|
||||||
if (!tokenInfo) return
|
|
||||||
|
|
||||||
const credentials = geminiOAuth.buildCredentials(tokenInfo)
|
|
||||||
|
|
||||||
// Note: intercept_warmup_requests is Anthropic-only, not applicable to Gemini
|
|
||||||
|
|
||||||
await adminAPI.accounts.create({
|
|
||||||
name: form.name,
|
|
||||||
platform: 'gemini',
|
|
||||||
type: 'oauth',
|
|
||||||
credentials,
|
|
||||||
proxy_id: form.proxy_id,
|
|
||||||
concurrency: form.concurrency,
|
|
||||||
priority: form.priority,
|
|
||||||
group_ids: form.group_ids
|
|
||||||
})
|
|
||||||
|
|
||||||
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
|
||||||
emit('created')
|
|
||||||
handleClose()
|
|
||||||
} catch (error: any) {
|
|
||||||
geminiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
|
||||||
appStore.showError(geminiOAuth.error.value)
|
appStore.showError(geminiOAuth.error.value)
|
||||||
} finally {
|
return
|
||||||
geminiOAuth.loading.value = false
|
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For Anthropic
|
const tokenInfo = await geminiOAuth.exchangeAuthCode({
|
||||||
|
code: authCode.trim(),
|
||||||
|
sessionId: geminiOAuth.sessionId.value,
|
||||||
|
state: stateToUse,
|
||||||
|
proxyId: form.proxy_id,
|
||||||
|
oauthType: geminiOAuthType.value
|
||||||
|
})
|
||||||
|
if (!tokenInfo) return
|
||||||
|
|
||||||
|
const credentials = geminiOAuth.buildCredentials(tokenInfo)
|
||||||
|
await createAccountAndFinish('gemini', 'oauth', credentials)
|
||||||
|
} catch (error: any) {
|
||||||
|
geminiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
||||||
|
appStore.showError(geminiOAuth.error.value)
|
||||||
|
} finally {
|
||||||
|
geminiOAuth.loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Antigravity OAuth 授权码兑换
|
||||||
|
const handleAntigravityExchange = async (authCode: string) => {
|
||||||
|
if (!authCode.trim() || !antigravityOAuth.sessionId.value) return
|
||||||
|
|
||||||
|
antigravityOAuth.loading.value = true
|
||||||
|
antigravityOAuth.error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stateFromInput = oauthFlowRef.value?.oauthState || ''
|
||||||
|
const stateToUse = stateFromInput || antigravityOAuth.state.value
|
||||||
|
if (!stateToUse) {
|
||||||
|
antigravityOAuth.error.value = t('admin.accounts.oauth.authFailed')
|
||||||
|
appStore.showError(antigravityOAuth.error.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenInfo = await antigravityOAuth.exchangeAuthCode({
|
||||||
|
code: authCode.trim(),
|
||||||
|
sessionId: antigravityOAuth.sessionId.value,
|
||||||
|
state: stateToUse,
|
||||||
|
proxyId: form.proxy_id
|
||||||
|
})
|
||||||
|
if (!tokenInfo) return
|
||||||
|
|
||||||
|
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
|
||||||
|
await createAccountAndFinish('antigravity', 'oauth', credentials)
|
||||||
|
} catch (error: any) {
|
||||||
|
antigravityOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
||||||
|
appStore.showError(antigravityOAuth.error.value)
|
||||||
|
} finally {
|
||||||
|
antigravityOAuth.loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anthropic OAuth 授权码兑换
|
||||||
|
const handleAnthropicExchange = async (authCode: string) => {
|
||||||
if (!authCode.trim() || !oauth.sessionId.value) return
|
if (!authCode.trim() || !oauth.sessionId.value) return
|
||||||
|
|
||||||
oauth.loading.value = true
|
oauth.loading.value = true
|
||||||
@@ -1755,28 +1846,11 @@ const handleExchangeCode = async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const extra = oauth.buildExtraInfo(tokenInfo)
|
const extra = oauth.buildExtraInfo(tokenInfo)
|
||||||
|
|
||||||
// Merge interceptWarmupRequests into credentials
|
|
||||||
const credentials = {
|
const credentials = {
|
||||||
...tokenInfo,
|
...tokenInfo,
|
||||||
...(interceptWarmupRequests.value ? { intercept_warmup_requests: true } : {})
|
...(interceptWarmupRequests.value ? { intercept_warmup_requests: true } : {})
|
||||||
}
|
}
|
||||||
|
await createAccountAndFinish(form.platform, addMethod.value as AccountType, credentials, extra)
|
||||||
await adminAPI.accounts.create({
|
|
||||||
name: form.name,
|
|
||||||
platform: form.platform,
|
|
||||||
type: addMethod.value, // Use addMethod as type: 'oauth' or 'setup-token'
|
|
||||||
credentials,
|
|
||||||
extra,
|
|
||||||
proxy_id: form.proxy_id,
|
|
||||||
concurrency: form.concurrency,
|
|
||||||
priority: form.priority,
|
|
||||||
group_ids: form.group_ids
|
|
||||||
})
|
|
||||||
|
|
||||||
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
|
||||||
emit('created')
|
|
||||||
handleClose()
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
oauth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
oauth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
||||||
appStore.showError(oauth.error.value)
|
appStore.showError(oauth.error.value)
|
||||||
@@ -1785,6 +1859,22 @@ const handleExchangeCode = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 主入口:根据平台路由到对应处理函数
|
||||||
|
const handleExchangeCode = async () => {
|
||||||
|
const authCode = oauthFlowRef.value?.authCode || ''
|
||||||
|
|
||||||
|
switch (form.platform) {
|
||||||
|
case 'openai':
|
||||||
|
return handleOpenAIExchange(authCode)
|
||||||
|
case 'gemini':
|
||||||
|
return handleGeminiExchange(authCode)
|
||||||
|
case 'antigravity':
|
||||||
|
return handleAntigravityExchange(authCode)
|
||||||
|
default:
|
||||||
|
return handleAnthropicExchange(authCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleCookieAuth = async (sessionKey: string) => {
|
const handleCookieAuth = async (sessionKey: string) => {
|
||||||
oauth.loading.value = true
|
oauth.loading.value = true
|
||||||
oauth.error.value = ''
|
oauth.error.value = ''
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ interface Props {
|
|||||||
allowMultiple?: boolean
|
allowMultiple?: boolean
|
||||||
methodLabel?: string
|
methodLabel?: string
|
||||||
showCookieOption?: boolean // Whether to show cookie auto-auth option
|
showCookieOption?: boolean // Whether to show cookie auto-auth option
|
||||||
platform?: 'anthropic' | 'openai' | 'gemini' // Platform type for different UI/text
|
platform?: 'anthropic' | 'openai' | 'gemini' | 'antigravity' // Platform type for different UI/text
|
||||||
showProjectId?: boolean // New prop to control project ID visibility
|
showProjectId?: boolean // New prop to control project ID visibility
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,6 +560,7 @@ const isOpenAI = computed(() => props.platform === 'openai')
|
|||||||
const getOAuthKey = (key: string) => {
|
const getOAuthKey = (key: string) => {
|
||||||
if (props.platform === 'openai') return `admin.accounts.oauth.openai.${key}`
|
if (props.platform === 'openai') return `admin.accounts.oauth.openai.${key}`
|
||||||
if (props.platform === 'gemini') return `admin.accounts.oauth.gemini.${key}`
|
if (props.platform === 'gemini') return `admin.accounts.oauth.gemini.${key}`
|
||||||
|
if (props.platform === 'antigravity') return `admin.accounts.oauth.antigravity.${key}`
|
||||||
return `admin.accounts.oauth.${key}`
|
return `admin.accounts.oauth.${key}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,9 +576,11 @@ const oauthAuthCodeDesc = computed(() => t(getOAuthKey('authCodeDesc')))
|
|||||||
const oauthAuthCode = computed(() => t(getOAuthKey('authCode')))
|
const oauthAuthCode = computed(() => t(getOAuthKey('authCode')))
|
||||||
const oauthAuthCodePlaceholder = computed(() => t(getOAuthKey('authCodePlaceholder')))
|
const oauthAuthCodePlaceholder = computed(() => t(getOAuthKey('authCodePlaceholder')))
|
||||||
const oauthAuthCodeHint = computed(() => t(getOAuthKey('authCodeHint')))
|
const oauthAuthCodeHint = computed(() => t(getOAuthKey('authCodeHint')))
|
||||||
const oauthImportantNotice = computed(() =>
|
const oauthImportantNotice = computed(() => {
|
||||||
props.platform === 'openai' ? t('admin.accounts.oauth.openai.importantNotice') : ''
|
if (props.platform === 'openai') return t('admin.accounts.oauth.openai.importantNotice')
|
||||||
)
|
if (props.platform === 'antigravity') return t('admin.accounts.oauth.antigravity.importantNotice')
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
// Local state
|
// Local state
|
||||||
const inputMethod = ref<AuthInputMethod>(props.showCookieOption ? 'manual' : 'manual')
|
const inputMethod = ref<AuthInputMethod>(props.showCookieOption ? 'manual' : 'manual')
|
||||||
@@ -603,10 +606,10 @@ watch(inputMethod, (newVal) => {
|
|||||||
emit('update:inputMethod', newVal)
|
emit('update:inputMethod', newVal)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Auto-extract code from OpenAI callback URL
|
// Auto-extract code from callback URL (OpenAI/Gemini/Antigravity)
|
||||||
// e.g., http://localhost:1455/auth/callback?code=ac_xxx...&scope=...&state=...
|
// e.g., http://localhost:8085/callback?code=xxx...&state=...
|
||||||
watch(authCodeInput, (newVal) => {
|
watch(authCodeInput, (newVal) => {
|
||||||
if (props.platform !== 'openai' && props.platform !== 'gemini') return
|
if (props.platform !== 'openai' && props.platform !== 'gemini' && props.platform !== 'antigravity') return
|
||||||
|
|
||||||
const trimmed = newVal.trim()
|
const trimmed = newVal.trim()
|
||||||
// Check if it looks like a URL with code parameter
|
// Check if it looks like a URL with code parameter
|
||||||
@@ -616,7 +619,7 @@ watch(authCodeInput, (newVal) => {
|
|||||||
const url = new URL(trimmed)
|
const url = new URL(trimmed)
|
||||||
const code = url.searchParams.get('code')
|
const code = url.searchParams.get('code')
|
||||||
const stateParam = url.searchParams.get('state')
|
const stateParam = url.searchParams.get('state')
|
||||||
if (props.platform === 'gemini' && stateParam) {
|
if ((props.platform === 'gemini' || props.platform === 'antigravity') && stateParam) {
|
||||||
oauthState.value = stateParam
|
oauthState.value = stateParam
|
||||||
}
|
}
|
||||||
if (code && code !== trimmed) {
|
if (code && code !== trimmed) {
|
||||||
@@ -627,7 +630,7 @@ watch(authCodeInput, (newVal) => {
|
|||||||
// If URL parsing fails, try regex extraction
|
// If URL parsing fails, try regex extraction
|
||||||
const match = trimmed.match(/[?&]code=([^&]+)/)
|
const match = trimmed.match(/[?&]code=([^&]+)/)
|
||||||
const stateMatch = trimmed.match(/[?&]state=([^&]+)/)
|
const stateMatch = trimmed.match(/[?&]state=([^&]+)/)
|
||||||
if (props.platform === 'gemini' && stateMatch && stateMatch[1]) {
|
if ((props.platform === 'gemini' || props.platform === 'antigravity') && stateMatch && stateMatch[1]) {
|
||||||
oauthState.value = stateMatch[1]
|
oauthState.value = stateMatch[1]
|
||||||
}
|
}
|
||||||
if (match && match[1] && match[1] !== trimmed) {
|
if (match && match[1] && match[1] !== trimmed) {
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
<svg v-else-if="platform === 'gemini'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
|
<svg v-else-if="platform === 'gemini'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
|
||||||
<path d="M12 2l1.89 7.2L21 12l-7.11 2.8L12 22l-1.89-7.2L3 12l7.11-2.8L12 2z" />
|
<path d="M12 2l1.89 7.2L21 12l-7.11 2.8L12 22l-1.89-7.2L3 12l7.11-2.8L12 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
<!-- Antigravity logo (cloud) -->
|
||||||
|
<svg v-else-if="platform === 'antigravity'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z" />
|
||||||
|
</svg>
|
||||||
<!-- Fallback: generic platform icon -->
|
<!-- Fallback: generic platform icon -->
|
||||||
<svg v-else :class="sizeClass" fill="currentColor" viewBox="0 0 24 24">
|
<svg v-else :class="sizeClass" fill="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ const props = defineProps<Props>()
|
|||||||
const platformLabel = computed(() => {
|
const platformLabel = computed(() => {
|
||||||
if (props.platform === 'anthropic') return 'Anthropic'
|
if (props.platform === 'anthropic') return 'Anthropic'
|
||||||
if (props.platform === 'openai') return 'OpenAI'
|
if (props.platform === 'openai') return 'OpenAI'
|
||||||
|
if (props.platform === 'antigravity') return 'Antigravity'
|
||||||
return 'Gemini'
|
return 'Gemini'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -95,6 +96,9 @@ const platformClass = computed(() => {
|
|||||||
if (props.platform === 'openai') {
|
if (props.platform === 'openai') {
|
||||||
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||||||
}
|
}
|
||||||
|
if (props.platform === 'antigravity') {
|
||||||
|
return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
||||||
|
}
|
||||||
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -105,6 +109,9 @@ const typeClass = computed(() => {
|
|||||||
if (props.platform === 'openai') {
|
if (props.platform === 'openai') {
|
||||||
return 'bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400'
|
return 'bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||||||
}
|
}
|
||||||
|
if (props.platform === 'antigravity') {
|
||||||
|
return 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400'
|
||||||
|
}
|
||||||
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
|
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
115
frontend/src/composables/useAntigravityOAuth.ts
Normal file
115
frontend/src/composables/useAntigravityOAuth.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { adminAPI } from '@/api/admin'
|
||||||
|
import type { AntigravityTokenInfo } from '@/api/admin/antigravity'
|
||||||
|
|
||||||
|
export function useAntigravityOAuth() {
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const authUrl = ref('')
|
||||||
|
const sessionId = ref('')
|
||||||
|
const state = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
const resetState = () => {
|
||||||
|
authUrl.value = ''
|
||||||
|
sessionId.value = ''
|
||||||
|
state.value = ''
|
||||||
|
loading.value = false
|
||||||
|
error.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateAuthUrl = async (proxyId: number | null | undefined): Promise<boolean> => {
|
||||||
|
loading.value = true
|
||||||
|
authUrl.value = ''
|
||||||
|
sessionId.value = ''
|
||||||
|
state.value = ''
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload: Record<string, unknown> = {}
|
||||||
|
if (proxyId) payload.proxy_id = proxyId
|
||||||
|
|
||||||
|
const response = await adminAPI.antigravity.generateAuthUrl(payload as any)
|
||||||
|
authUrl.value = response.auth_url
|
||||||
|
sessionId.value = response.session_id
|
||||||
|
state.value = response.state
|
||||||
|
return true
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value =
|
||||||
|
err.response?.data?.detail || t('admin.accounts.oauth.antigravity.failedToGenerateUrl')
|
||||||
|
appStore.showError(error.value)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exchangeAuthCode = async (params: {
|
||||||
|
code: string
|
||||||
|
sessionId: string
|
||||||
|
state: string
|
||||||
|
proxyId?: number | null
|
||||||
|
}): Promise<AntigravityTokenInfo | null> => {
|
||||||
|
const code = params.code?.trim()
|
||||||
|
if (!code || !params.sessionId || !params.state) {
|
||||||
|
error.value = t('admin.accounts.oauth.antigravity.missingExchangeParams')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload: Record<string, unknown> = {
|
||||||
|
session_id: params.sessionId,
|
||||||
|
state: params.state,
|
||||||
|
code
|
||||||
|
}
|
||||||
|
if (params.proxyId) payload.proxy_id = params.proxyId
|
||||||
|
|
||||||
|
const tokenInfo = await adminAPI.antigravity.exchangeCode(payload as any)
|
||||||
|
return tokenInfo as AntigravityTokenInfo
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value =
|
||||||
|
err.response?.data?.detail || t('admin.accounts.oauth.antigravity.failedToExchangeCode')
|
||||||
|
appStore.showError(error.value)
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildCredentials = (tokenInfo: AntigravityTokenInfo): Record<string, unknown> => {
|
||||||
|
let expiresAt: string | undefined
|
||||||
|
if (typeof tokenInfo.expires_at === 'number' && Number.isFinite(tokenInfo.expires_at)) {
|
||||||
|
expiresAt = Math.floor(tokenInfo.expires_at).toString()
|
||||||
|
} else if (typeof tokenInfo.expires_at === 'string' && tokenInfo.expires_at.trim()) {
|
||||||
|
expiresAt = tokenInfo.expires_at.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
access_token: tokenInfo.access_token,
|
||||||
|
refresh_token: tokenInfo.refresh_token,
|
||||||
|
token_type: tokenInfo.token_type,
|
||||||
|
expires_at: expiresAt,
|
||||||
|
project_id: tokenInfo.project_id,
|
||||||
|
email: tokenInfo.email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
authUrl,
|
||||||
|
sessionId,
|
||||||
|
state,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
resetState,
|
||||||
|
generateAuthUrl,
|
||||||
|
exchangeAuthCode,
|
||||||
|
buildCredentials
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -820,14 +820,16 @@ export default {
|
|||||||
anthropic: 'Anthropic',
|
anthropic: 'Anthropic',
|
||||||
claude: 'Claude',
|
claude: 'Claude',
|
||||||
openai: 'OpenAI',
|
openai: 'OpenAI',
|
||||||
gemini: 'Gemini'
|
gemini: 'Gemini',
|
||||||
|
antigravity: 'Antigravity'
|
||||||
},
|
},
|
||||||
types: {
|
types: {
|
||||||
oauth: 'OAuth',
|
oauth: 'OAuth',
|
||||||
chatgptOauth: 'ChatGPT OAuth',
|
chatgptOauth: 'ChatGPT OAuth',
|
||||||
responsesApi: 'Responses API',
|
responsesApi: 'Responses API',
|
||||||
googleOauth: 'Google OAuth',
|
googleOauth: 'Google OAuth',
|
||||||
codeAssist: 'Code Assist'
|
codeAssist: 'Code Assist',
|
||||||
|
antigravityOauth: 'Antigravity OAuth'
|
||||||
},
|
},
|
||||||
columns: {
|
columns: {
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
@@ -1056,7 +1058,28 @@ export default {
|
|||||||
'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback (Consent screen scopes must include https://www.googleapis.com/auth/generative-language.retriever)',
|
'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback (Consent screen scopes must include https://www.googleapis.com/auth/generative-language.retriever)',
|
||||||
aiStudioNotConfigured:
|
aiStudioNotConfigured:
|
||||||
'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback'
|
'AI Studio OAuth is not configured: set GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET and add Redirect URI: http://localhost:1455/auth/callback'
|
||||||
}
|
},
|
||||||
|
// Antigravity specific
|
||||||
|
antigravity: {
|
||||||
|
title: 'Antigravity Account Authorization',
|
||||||
|
followSteps: 'Follow these steps to authorize your Antigravity account:',
|
||||||
|
step1GenerateUrl: 'Generate the authorization URL',
|
||||||
|
generateAuthUrl: 'Generate Auth URL',
|
||||||
|
step2OpenUrl: 'Open the URL in your browser and complete authorization',
|
||||||
|
openUrlDesc: 'Open the authorization URL in a new tab, log in to your Google account and authorize.',
|
||||||
|
importantNotice:
|
||||||
|
'<strong>Important:</strong> The page may take a while to load after authorization. Please wait patiently. When the browser address bar shows <code>http://localhost...</code>, authorization is complete.',
|
||||||
|
step3EnterCode: 'Enter Authorization URL or Code',
|
||||||
|
authCodeDesc:
|
||||||
|
'After authorization, when the page URL becomes <code>http://localhost:xxx/auth/callback?code=...</code>:',
|
||||||
|
authCode: 'Authorization URL or Code',
|
||||||
|
authCodePlaceholder:
|
||||||
|
'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value',
|
||||||
|
authCodeHint: 'You can copy the entire URL or just the code parameter value, the system will auto-detect',
|
||||||
|
failedToGenerateUrl: 'Failed to generate Antigravity auth URL',
|
||||||
|
missingExchangeParams: 'Missing code, session ID, or state',
|
||||||
|
failedToExchangeCode: 'Failed to exchange Antigravity auth code'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// Gemini specific (platform-wide)
|
// Gemini specific (platform-wide)
|
||||||
gemini: {
|
gemini: {
|
||||||
@@ -1070,6 +1093,7 @@ export default {
|
|||||||
claudeCodeAccount: 'Claude Code Account',
|
claudeCodeAccount: 'Claude Code Account',
|
||||||
openaiAccount: 'OpenAI Account',
|
openaiAccount: 'OpenAI Account',
|
||||||
geminiAccount: 'Gemini Account',
|
geminiAccount: 'Gemini Account',
|
||||||
|
antigravityAccount: 'Antigravity Account',
|
||||||
inputMethod: 'Input Method',
|
inputMethod: 'Input Method',
|
||||||
reAuthorizedSuccess: 'Account re-authorized successfully',
|
reAuthorizedSuccess: 'Account re-authorized successfully',
|
||||||
// Test Modal
|
// Test Modal
|
||||||
|
|||||||
@@ -940,7 +940,8 @@ export default {
|
|||||||
claude: 'Claude',
|
claude: 'Claude',
|
||||||
openai: 'OpenAI',
|
openai: 'OpenAI',
|
||||||
anthropic: 'Anthropic',
|
anthropic: 'Anthropic',
|
||||||
gemini: 'Gemini'
|
gemini: 'Gemini',
|
||||||
|
antigravity: 'Antigravity'
|
||||||
},
|
},
|
||||||
types: {
|
types: {
|
||||||
oauth: 'OAuth',
|
oauth: 'OAuth',
|
||||||
@@ -948,6 +949,7 @@ export default {
|
|||||||
responsesApi: 'Responses API',
|
responsesApi: 'Responses API',
|
||||||
googleOauth: 'Google OAuth',
|
googleOauth: 'Google OAuth',
|
||||||
codeAssist: 'Code Assist',
|
codeAssist: 'Code Assist',
|
||||||
|
antigravityOauth: 'Antigravity OAuth',
|
||||||
api_key: 'API Key',
|
api_key: 'API Key',
|
||||||
cookie: 'Cookie'
|
cookie: 'Cookie'
|
||||||
},
|
},
|
||||||
@@ -1178,7 +1180,28 @@ export default {
|
|||||||
aiStudioNotConfiguredShort: '未配置',
|
aiStudioNotConfiguredShort: '未配置',
|
||||||
aiStudioNotConfiguredTip: 'AI Studio OAuth 未配置:请先设置 GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET,并在 Google OAuth Client 添加 Redirect URI:http://localhost:1455/auth/callback(Consent Screen scopes 需包含 https://www.googleapis.com/auth/generative-language.retriever)',
|
aiStudioNotConfiguredTip: 'AI Studio OAuth 未配置:请先设置 GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET,并在 Google OAuth Client 添加 Redirect URI:http://localhost:1455/auth/callback(Consent Screen scopes 需包含 https://www.googleapis.com/auth/generative-language.retriever)',
|
||||||
aiStudioNotConfigured: 'AI Studio OAuth 未配置:请先设置 GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET,并在 Google OAuth Client 添加 Redirect URI:http://localhost:1455/auth/callback'
|
aiStudioNotConfigured: 'AI Studio OAuth 未配置:请先设置 GEMINI_OAUTH_CLIENT_ID / GEMINI_OAUTH_CLIENT_SECRET,并在 Google OAuth Client 添加 Redirect URI:http://localhost:1455/auth/callback'
|
||||||
}
|
},
|
||||||
|
// Antigravity specific
|
||||||
|
antigravity: {
|
||||||
|
title: 'Antigravity 账户授权',
|
||||||
|
followSteps: '请按照以下步骤完成 Antigravity 账户的授权:',
|
||||||
|
step1GenerateUrl: '生成授权链接',
|
||||||
|
generateAuthUrl: '生成授权链接',
|
||||||
|
step2OpenUrl: '在浏览器中打开链接并完成授权',
|
||||||
|
openUrlDesc: '请在新标签页中打开授权链接,登录您的 Google 账户并授权。',
|
||||||
|
importantNotice:
|
||||||
|
'<strong>重要提示:</strong>授权后页面可能会加载较长时间,请耐心等待。当浏览器地址栏变为 <code>http://localhost...</code> 开头时,表示授权已完成。',
|
||||||
|
step3EnterCode: '输入授权链接或 Code',
|
||||||
|
authCodeDesc:
|
||||||
|
'授权完成后,当页面地址变为 <code>http://localhost:xxx/auth/callback?code=...</code> 时:',
|
||||||
|
authCode: '授权链接或 Code',
|
||||||
|
authCodePlaceholder:
|
||||||
|
'方式1:复制完整的链接\n(http://localhost:xxx/auth/callback?code=...)\n方式2:仅复制 code 参数的值',
|
||||||
|
authCodeHint: '您可以直接复制整个链接或仅复制 code 参数值,系统会自动识别',
|
||||||
|
failedToGenerateUrl: '生成 Antigravity 授权链接失败',
|
||||||
|
missingExchangeParams: '缺少 code / session_id / state',
|
||||||
|
failedToExchangeCode: 'Antigravity 授权码兑换失败'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// Gemini specific (platform-wide)
|
// Gemini specific (platform-wide)
|
||||||
gemini: {
|
gemini: {
|
||||||
@@ -1191,6 +1214,7 @@ export default {
|
|||||||
claudeCodeAccount: 'Claude Code 账号',
|
claudeCodeAccount: 'Claude Code 账号',
|
||||||
openaiAccount: 'OpenAI 账号',
|
openaiAccount: 'OpenAI 账号',
|
||||||
geminiAccount: 'Gemini 账号',
|
geminiAccount: 'Gemini 账号',
|
||||||
|
antigravityAccount: 'Antigravity 账号',
|
||||||
inputMethod: '输入方式',
|
inputMethod: '输入方式',
|
||||||
reAuthorizedSuccess: '账号重新授权成功',
|
reAuthorizedSuccess: '账号重新授权成功',
|
||||||
// Test Modal
|
// Test Modal
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ export interface PaginationConfig {
|
|||||||
|
|
||||||
// ==================== API Key & Group Types ====================
|
// ==================== API Key & Group Types ====================
|
||||||
|
|
||||||
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini'
|
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity'
|
||||||
|
|
||||||
export type SubscriptionType = 'standard' | 'subscription'
|
export type SubscriptionType = 'standard' | 'subscription'
|
||||||
|
|
||||||
@@ -256,7 +256,7 @@ export interface ApiKey {
|
|||||||
export interface CreateApiKeyRequest {
|
export interface CreateApiKeyRequest {
|
||||||
name: string
|
name: string
|
||||||
group_id?: number | null
|
group_id?: number | null
|
||||||
custom_key?: string // 可选的自定义API Key
|
custom_key?: string // Optional custom API Key
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateApiKeyRequest {
|
export interface UpdateApiKeyRequest {
|
||||||
@@ -284,7 +284,7 @@ export interface UpdateGroupRequest {
|
|||||||
|
|
||||||
// ==================== Account & Proxy Types ====================
|
// ==================== Account & Proxy Types ====================
|
||||||
|
|
||||||
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini'
|
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity'
|
||||||
export type AccountType = 'oauth' | 'setup-token' | 'apikey'
|
export type AccountType = 'oauth' | 'setup-token' | 'apikey'
|
||||||
export type OAuthAddMethod = 'oauth' | 'setup-token'
|
export type OAuthAddMethod = 'oauth' | 'setup-token'
|
||||||
export type ProxyProtocol = 'http' | 'https' | 'socks5'
|
export type ProxyProtocol = 'http' | 'https' | 'socks5'
|
||||||
|
|||||||
@@ -594,7 +594,8 @@ const platformOptions = computed(() => [
|
|||||||
{ value: '', label: t('admin.accounts.allPlatforms') },
|
{ value: '', label: t('admin.accounts.allPlatforms') },
|
||||||
{ value: 'anthropic', label: t('admin.accounts.platforms.anthropic') },
|
{ value: 'anthropic', label: t('admin.accounts.platforms.anthropic') },
|
||||||
{ value: 'openai', label: t('admin.accounts.platforms.openai') },
|
{ value: 'openai', label: t('admin.accounts.platforms.openai') },
|
||||||
{ value: 'gemini', label: t('admin.accounts.platforms.gemini') }
|
{ value: 'gemini', label: t('admin.accounts.platforms.gemini') },
|
||||||
|
{ value: 'antigravity', label: t('admin.accounts.platforms.antigravity') }
|
||||||
])
|
])
|
||||||
|
|
||||||
const typeOptions = computed(() => [
|
const typeOptions = computed(() => [
|
||||||
|
|||||||
Reference in New Issue
Block a user