2025-12-28 15:54:42 +08:00
|
|
|
|
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 端点
|
2026-01-08 17:27:35 +08:00
|
|
|
|
// 优先使用 sandbox daily URL,配额更宽松
|
|
|
|
|
|
BaseURL = "https://daily-cloudcode-pa.sandbox.googleapis.com"
|
2025-12-28 15:54:42 +08:00
|
|
|
|
|
2026-01-08 17:27:35 +08:00
|
|
|
|
// User-Agent(模拟官方客户端)
|
|
|
|
|
|
UserAgent = "antigravity/1.104.0 darwin/arm64"
|
2025-12-28 15:54:42 +08:00
|
|
|
|
|
|
|
|
|
|
// 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())
|
|
|
|
|
|
}
|
2025-12-30 23:54:33 +08:00
|
|
|
|
|
|
|
|
|
|
// GenerateMockProjectID 生成随机 project_id(当 API 不返回时使用)
|
|
|
|
|
|
// 格式:{形容词}-{名词}-{5位随机字符}
|
|
|
|
|
|
func GenerateMockProjectID() string {
|
|
|
|
|
|
adjectives := []string{"useful", "bright", "swift", "calm", "bold"}
|
|
|
|
|
|
nouns := []string{"fuze", "wave", "spark", "flow", "core"}
|
|
|
|
|
|
|
|
|
|
|
|
randBytes, _ := GenerateRandomBytes(7)
|
|
|
|
|
|
|
|
|
|
|
|
adj := adjectives[int(randBytes[0])%len(adjectives)]
|
|
|
|
|
|
noun := nouns[int(randBytes[1])%len(nouns)]
|
|
|
|
|
|
|
|
|
|
|
|
// 生成 5 位随机字符(a-z0-9)
|
|
|
|
|
|
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
|
|
|
|
suffix := make([]byte, 5)
|
|
|
|
|
|
for i := 0; i < 5; i++ {
|
|
|
|
|
|
suffix[i] = charset[int(randBytes[i+2])%len(charset)]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return fmt.Sprintf("%s-%s-%s", adj, noun, string(suffix))
|
|
|
|
|
|
}
|