2026-04-02 14:45:41 +00:00
|
|
|
package geminiweb
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"cursor-api-proxy/internal/apitypes"
|
|
|
|
|
"cursor-api-proxy/internal/config"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"os/signal"
|
|
|
|
|
"syscall"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/go-rod/rod"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Provider struct {
|
|
|
|
|
cfg config.BridgeConfig
|
|
|
|
|
pool *SessionPool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewProvider(cfg config.BridgeConfig) *Provider {
|
|
|
|
|
return &Provider{cfg: cfg}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Provider) Name() string {
|
|
|
|
|
return "gemini-web"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Provider) Close() error {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Provider) initPool() error {
|
|
|
|
|
if p.pool != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
pool, err := NewSessionPool(p.cfg.GeminiAccountDir, p.cfg.GeminiMaxSessions)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to init session pool: %w", err)
|
|
|
|
|
}
|
|
|
|
|
p.pool = pool
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Provider) Generate(ctx context.Context, model string, messages []apitypes.Message, tools []apitypes.Tool, cb func(apitypes.StreamChunk)) error {
|
2026-04-02 16:40:57 +00:00
|
|
|
fmt.Printf("[GeminiWeb] Starting generation with model: %s\n", model)
|
|
|
|
|
|
2026-04-02 14:45:41 +00:00
|
|
|
if err := p.initPool(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
session := p.pool.GetAvailable()
|
|
|
|
|
if session == nil {
|
2026-04-02 16:40:57 +00:00
|
|
|
return fmt.Errorf("no available sessions - please run 'gemini-login <session-name>' first")
|
2026-04-02 14:45:41 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-02 16:40:57 +00:00
|
|
|
fmt.Printf("[GeminiWeb] Using session: %s\n", session.Name)
|
|
|
|
|
|
2026-04-02 14:45:41 +00:00
|
|
|
p.pool.StartSession(session)
|
|
|
|
|
defer p.pool.EndSession(session)
|
|
|
|
|
|
|
|
|
|
browser, err := NewBrowser(p.cfg.GeminiBrowserVisible)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create browser: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer browser.Close()
|
|
|
|
|
|
|
|
|
|
page, err := browser.NewPage()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create page: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if session.CookieFile != "" {
|
2026-04-02 16:40:57 +00:00
|
|
|
fmt.Printf("[GeminiWeb] Loading cookies from: %s\n", session.CookieFile)
|
2026-04-02 14:45:41 +00:00
|
|
|
cookies, err := LoadCookiesFromFile(session.CookieFile)
|
|
|
|
|
if err == nil {
|
|
|
|
|
if err := SetCookiesOnPage(page, cookies); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to set cookies: %w", err)
|
|
|
|
|
}
|
2026-04-02 16:40:57 +00:00
|
|
|
fmt.Printf("[GeminiWeb] Loaded %d cookies\n", len(cookies))
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Printf("[GeminiWeb] Warning: could not load cookies: %v\n", err)
|
2026-04-02 14:45:41 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 16:40:57 +00:00
|
|
|
fmt.Printf("[GeminiWeb] Navigating to Gemini...\n")
|
2026-04-02 14:45:41 +00:00
|
|
|
if err := NavigateToGemini(page); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to navigate: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
|
|
2026-04-02 16:40:57 +00:00
|
|
|
fmt.Printf("[GeminiWeb] Checking login status...\n")
|
2026-04-02 14:45:41 +00:00
|
|
|
if !IsLoggedIn(page) {
|
2026-04-02 16:40:57 +00:00
|
|
|
return fmt.Errorf("session not logged in - please run 'gemini-login %s' first", session.Name)
|
2026-04-02 14:45:41 +00:00
|
|
|
}
|
2026-04-02 16:40:57 +00:00
|
|
|
fmt.Printf("[GeminiWeb] Logged in successfully\n")
|
2026-04-02 14:45:41 +00:00
|
|
|
|
2026-04-02 16:40:57 +00:00
|
|
|
fmt.Printf("[GeminiWeb] Selecting model: %s\n", model)
|
2026-04-02 14:45:41 +00:00
|
|
|
if err := SelectModel(page, model); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to select model: %w", err)
|
|
|
|
|
}
|
2026-04-02 16:40:57 +00:00
|
|
|
fmt.Printf("[GeminiWeb] Model selected\n")
|
2026-04-02 14:45:41 +00:00
|
|
|
|
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
|
|
|
|
|
|
prompt := buildPromptFromMessages(messages)
|
2026-04-02 16:40:57 +00:00
|
|
|
fmt.Printf("[GeminiWeb] Sending prompt (length: %d chars)\n", len(prompt))
|
2026-04-02 14:45:41 +00:00
|
|
|
if err := SendPrompt(page, prompt); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to send prompt: %w", err)
|
|
|
|
|
}
|
2026-04-02 16:40:57 +00:00
|
|
|
fmt.Printf("[GeminiWeb] Prompt sent, waiting for response...\n")
|
2026-04-02 14:45:41 +00:00
|
|
|
|
|
|
|
|
return WaitForResponse(page,
|
|
|
|
|
func(text string) {
|
|
|
|
|
cb(apitypes.StreamChunk{Type: apitypes.ChunkText, Text: text})
|
|
|
|
|
},
|
|
|
|
|
func(thinking string) {
|
|
|
|
|
cb(apitypes.StreamChunk{Type: apitypes.ChunkThinking, Thinking: thinking})
|
|
|
|
|
},
|
|
|
|
|
func() {
|
|
|
|
|
cb(apitypes.StreamChunk{Type: apitypes.ChunkDone, Done: true})
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func buildPromptFromMessages(messages []apitypes.Message) string {
|
|
|
|
|
var prompt string
|
|
|
|
|
for _, m := range messages {
|
|
|
|
|
switch m.Role {
|
|
|
|
|
case "system":
|
|
|
|
|
prompt += "System: " + m.Content + "\n\n"
|
|
|
|
|
case "user":
|
|
|
|
|
prompt += m.Content + "\n\n"
|
|
|
|
|
case "assistant":
|
|
|
|
|
prompt += "Assistant: " + m.Content + "\n\n"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return prompt
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func RunLogin(cfg config.BridgeConfig, sessionName string) error {
|
|
|
|
|
if sessionName == "" {
|
|
|
|
|
sessionName = fmt.Sprintf("session-%d", time.Now().Unix())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pool, err := NewSessionPool(cfg.GeminiAccountDir, cfg.GeminiMaxSessions)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to init pool: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
session, err := pool.CreateSession(sessionName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create session: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Starting browser for login. Session: %s\n", sessionName)
|
|
|
|
|
fmt.Println("Please log in to your Gemini account in the browser window.")
|
|
|
|
|
fmt.Println("Press Ctrl+C when you have completed the login...")
|
|
|
|
|
|
|
|
|
|
browser, err := NewBrowser(true)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create browser: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer browser.Close()
|
|
|
|
|
|
|
|
|
|
page, err := browser.NewPage()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create page: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := NavigateToGemini(page); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to navigate: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sigChan := make(chan os.Signal, 1)
|
|
|
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
|
|
|
|
|
|
|
|
<-sigChan
|
|
|
|
|
|
|
|
|
|
cookies, err := GetPageCookies(page)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to get cookies: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := SaveCookiesToFile(cookies, session.CookieFile); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to save cookies: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Session saved successfully: %s\n", sessionName)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func GetPageCookies(page *rod.Page) ([]Cookie, error) {
|
|
|
|
|
cookies, err := page.Cookies([]string{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to get cookies: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result []Cookie
|
|
|
|
|
for _, c := range cookies {
|
|
|
|
|
result = append(result, Cookie{
|
|
|
|
|
Name: c.Name,
|
|
|
|
|
Value: c.Value,
|
|
|
|
|
Domain: c.Domain,
|
|
|
|
|
Path: c.Path,
|
|
|
|
|
HTTPOnly: c.HTTPOnly,
|
|
|
|
|
Secure: c.Secure,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|