mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 21:10:51 +08:00
**feat(auth, executor, cmd): add Antigravity provider integration**
- Implemented OAuth login flow for the Antigravity provider in `auth/antigravity.go`. - Added `AntigravityExecutor` for handling requests and streaming via Antigravity APIs. - Created `antigravity_login.go` command for triggering Antigravity authentication. - Introduced OpenAI-to-Antigravity translation logic in `translator/antigravity/openai/chat-completions`. **refactor(translator, executor): update Gemini CLI response translation and add Antigravity payload customization** - Renamed Gemini CLI translation methods to align with response handling (`ConvertGeminiCliResponseToGemini` and `ConvertGeminiCliResponseToGeminiNonStream`). - Updated `init.go` to reflect these method changes. - Introduced `geminiToAntigravity` function to embed metadata (`model`, `userAgent`, `project`, etc.) into Antigravity payloads. - Added random project, request, and session ID generators for enhanced tracking. - Streamlined `buildRequest` to use `geminiToAntigravity` transformation before request execution.
This commit is contained in:
289
sdk/auth/antigravity.go
Normal file
289
sdk/auth/antigravity.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
|
||||
antigravityClientSecret = "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"
|
||||
)
|
||||
|
||||
var antigravityScopes = []string{
|
||||
"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",
|
||||
}
|
||||
|
||||
// AntigravityAuthenticator implements OAuth login for the antigravity provider.
|
||||
type AntigravityAuthenticator struct{}
|
||||
|
||||
// NewAntigravityAuthenticator constructs a new authenticator instance.
|
||||
func NewAntigravityAuthenticator() Authenticator { return &AntigravityAuthenticator{} }
|
||||
|
||||
// Provider returns the provider key for antigravity.
|
||||
func (AntigravityAuthenticator) Provider() string { return "antigravity" }
|
||||
|
||||
// RefreshLead instructs the manager to refresh five minutes before expiry.
|
||||
func (AntigravityAuthenticator) RefreshLead() *time.Duration {
|
||||
lead := 5 * time.Minute
|
||||
return &lead
|
||||
}
|
||||
|
||||
// Login launches a local OAuth flow to obtain antigravity tokens and persists them.
|
||||
func (AntigravityAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if opts == nil {
|
||||
opts = &LoginOptions{}
|
||||
}
|
||||
|
||||
state, err := misc.GenerateRandomState()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("antigravity: failed to generate state: %w", err)
|
||||
}
|
||||
|
||||
srv, port, cbChan, errServer := startAntigravityCallbackServer()
|
||||
if errServer != nil {
|
||||
return nil, fmt.Errorf("antigravity: failed to start callback server: %w", errServer)
|
||||
}
|
||||
defer func() {
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
_ = srv.Shutdown(shutdownCtx)
|
||||
}()
|
||||
|
||||
redirectURI := fmt.Sprintf("http://localhost:%d/oauth-callback", port)
|
||||
authURL := buildAntigravityAuthURL(redirectURI, state)
|
||||
|
||||
if !opts.NoBrowser {
|
||||
fmt.Println("Opening browser for antigravity authentication")
|
||||
if !browser.IsAvailable() {
|
||||
log.Warn("No browser available; please open the URL manually")
|
||||
util.PrintSSHTunnelInstructions(port)
|
||||
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
|
||||
} else if errOpen := browser.OpenURL(authURL); errOpen != nil {
|
||||
log.Warnf("Failed to open browser automatically: %v", errOpen)
|
||||
util.PrintSSHTunnelInstructions(port)
|
||||
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
|
||||
}
|
||||
} else {
|
||||
util.PrintSSHTunnelInstructions(port)
|
||||
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
|
||||
}
|
||||
|
||||
fmt.Println("Waiting for antigravity authentication callback...")
|
||||
|
||||
var cbRes callbackResult
|
||||
select {
|
||||
case res := <-cbChan:
|
||||
cbRes = res
|
||||
case <-time.After(5 * time.Minute):
|
||||
return nil, fmt.Errorf("antigravity: authentication timed out")
|
||||
}
|
||||
|
||||
if cbRes.Error != "" {
|
||||
return nil, fmt.Errorf("antigravity: authentication failed: %s", cbRes.Error)
|
||||
}
|
||||
if cbRes.State != state {
|
||||
return nil, fmt.Errorf("antigravity: invalid state")
|
||||
}
|
||||
if cbRes.Code == "" {
|
||||
return nil, fmt.Errorf("antigravity: missing authorization code")
|
||||
}
|
||||
|
||||
tokenResp, errToken := exchangeAntigravityCode(ctx, cbRes.Code, redirectURI)
|
||||
if errToken != nil {
|
||||
return nil, fmt.Errorf("antigravity: token exchange failed: %w", errToken)
|
||||
}
|
||||
|
||||
email := ""
|
||||
if tokenResp.AccessToken != "" {
|
||||
if info, errInfo := fetchAntigravityUserInfo(ctx, tokenResp.AccessToken); errInfo == nil && strings.TrimSpace(info.Email) != "" {
|
||||
email = strings.TrimSpace(info.Email)
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
metadata := map[string]any{
|
||||
"type": "antigravity",
|
||||
"access_token": tokenResp.AccessToken,
|
||||
"refresh_token": tokenResp.RefreshToken,
|
||||
"expires_in": tokenResp.ExpiresIn,
|
||||
"timestamp": now.UnixMilli(),
|
||||
"expired": now.Add(time.Duration(tokenResp.ExpiresIn) * time.Second).Format(time.RFC3339),
|
||||
}
|
||||
if email != "" {
|
||||
metadata["email"] = email
|
||||
}
|
||||
|
||||
fileName := sanitizeAntigravityFileName(email)
|
||||
label := email
|
||||
if label == "" {
|
||||
label = "antigravity"
|
||||
}
|
||||
|
||||
fmt.Println("Antigravity authentication successful")
|
||||
return &coreauth.Auth{
|
||||
ID: fileName,
|
||||
Provider: "antigravity",
|
||||
FileName: fileName,
|
||||
Label: label,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type callbackResult struct {
|
||||
Code string
|
||||
Error string
|
||||
State string
|
||||
}
|
||||
|
||||
func startAntigravityCallbackServer() (*http.Server, int, <-chan callbackResult, error) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, 0, nil, err
|
||||
}
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
resultCh := make(chan callbackResult, 1)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/oauth-callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
res := callbackResult{
|
||||
Code: strings.TrimSpace(q.Get("code")),
|
||||
Error: strings.TrimSpace(q.Get("error")),
|
||||
State: strings.TrimSpace(q.Get("state")),
|
||||
}
|
||||
resultCh <- res
|
||||
if res.Code != "" && res.Error == "" {
|
||||
_, _ = w.Write([]byte("<h1>Login successful</h1><p>You can close this window.</p>"))
|
||||
} else {
|
||||
_, _ = w.Write([]byte("<h1>Login failed</h1><p>Please check the CLI output.</p>"))
|
||||
}
|
||||
})
|
||||
|
||||
srv := &http.Server{Handler: mux}
|
||||
go func() {
|
||||
if errServe := srv.Serve(listener); errServe != nil && !strings.Contains(errServe.Error(), "Server closed") {
|
||||
log.Warnf("antigravity callback server error: %v", errServe)
|
||||
}
|
||||
}()
|
||||
|
||||
return srv, port, resultCh, nil
|
||||
}
|
||||
|
||||
type antigravityTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
func exchangeAntigravityCode(ctx context.Context, code, redirectURI string) (*antigravityTokenResponse, error) {
|
||||
data := url.Values{}
|
||||
data.Set("code", code)
|
||||
data.Set("client_id", antigravityClientID)
|
||||
data.Set("client_secret", antigravityClientSecret)
|
||||
data.Set("redirect_uri", redirectURI)
|
||||
data.Set("grant_type", "authorization_code")
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://oauth2.googleapis.com/token", strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, errDo := http.DefaultClient.Do(req)
|
||||
if errDo != nil {
|
||||
return nil, errDo
|
||||
}
|
||||
defer func() {
|
||||
if errClose := resp.Body.Close(); errClose != nil {
|
||||
log.Errorf("antigravity token exchange: close body error: %v", errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
var token antigravityTokenResponse
|
||||
if errDecode := json.NewDecoder(resp.Body).Decode(&token); errDecode != nil {
|
||||
return nil, errDecode
|
||||
}
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return nil, fmt.Errorf("oauth token exchange failed: status %d", resp.StatusCode)
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
type antigravityUserInfo struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func fetchAntigravityUserInfo(ctx context.Context, accessToken string) (*antigravityUserInfo, error) {
|
||||
if strings.TrimSpace(accessToken) == "" {
|
||||
return &antigravityUserInfo{}, nil
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.googleapis.com/oauth2/v1/userinfo?alt=json", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
|
||||
resp, errDo := http.DefaultClient.Do(req)
|
||||
if errDo != nil {
|
||||
return nil, errDo
|
||||
}
|
||||
defer func() {
|
||||
if errClose := resp.Body.Close(); errClose != nil {
|
||||
log.Errorf("antigravity userinfo: close body error: %v", errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return &antigravityUserInfo{}, nil
|
||||
}
|
||||
var info antigravityUserInfo
|
||||
if errDecode := json.NewDecoder(resp.Body).Decode(&info); errDecode != nil {
|
||||
return nil, errDecode
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func buildAntigravityAuthURL(redirectURI, state string) string {
|
||||
params := url.Values{}
|
||||
params.Set("access_type", "offline")
|
||||
params.Set("client_id", antigravityClientID)
|
||||
params.Set("prompt", "consent")
|
||||
params.Set("redirect_uri", redirectURI)
|
||||
params.Set("response_type", "code")
|
||||
params.Set("scope", strings.Join(antigravityScopes, " "))
|
||||
params.Set("state", state)
|
||||
return "https://accounts.google.com/o/oauth2/v2/auth?" + params.Encode()
|
||||
}
|
||||
|
||||
func sanitizeAntigravityFileName(email string) string {
|
||||
if strings.TrimSpace(email) == "" {
|
||||
return "antigravity.json"
|
||||
}
|
||||
replacer := strings.NewReplacer("@", "_", ".", "_")
|
||||
return fmt.Sprintf("antigravity-%s.json", replacer.Replace(email))
|
||||
}
|
||||
Reference in New Issue
Block a user