package geminiweb import ( "context" "cursor-api-proxy/internal/apitypes" "cursor-api-proxy/internal/config" "fmt" "os" "path/filepath" "strings" "sync" "time" ) // Provider 使用持久化瀏覽器管理器 type Provider struct { cfg config.BridgeConfig managerOnce sync.Once manager *BrowserManager managerErr error } // NewProvider 建立新的 Provider func NewProvider(cfg config.BridgeConfig) *Provider { return &Provider{cfg: cfg} } // getName 返回 Provider 名稱 func (p *Provider) Name() string { return "gemini-web" } // Close 關閉瀏覽器 func (p *Provider) Close() error { if p.manager != nil { return p.manager.Close() } return nil } // getManager 獲取或初始化瀏覽器管理器(單例) func (p *Provider) getManager() (*BrowserManager, error) { p.managerOnce.Do(func() { sessionDir := p.getSessionDir() p.manager, p.managerErr = GetBrowserManager(sessionDir, p.cfg.GeminiBrowserVisible) }) return p.manager, p.managerErr } // getSessionDir 獲取 session 目錄 func (p *Provider) getSessionDir() string { // 使用單一 session 目錄(簡化設計) return filepath.Join(p.cfg.GeminiAccountDir, "default-session") } // Generate 生成回應 func (p *Provider) Generate(ctx context.Context, model string, messages []apitypes.Message, tools []apitypes.Tool, cb func(apitypes.StreamChunk)) error { fmt.Printf("[GeminiWeb] Starting generation with model: %s\n", model) // 1. 獲取瀏覽器管理器 manager, err := p.getManager() if err != nil { return fmt.Errorf("failed to get browser manager: %w", err) } // 2. 啟動瀏覽器(如果尚未啟動) if !manager.IsRunning() { fmt.Printf("[GeminiWeb] Launching browser...\n") if err := manager.Launch(); err != nil { return fmt.Errorf("failed to launch browser: %w", err) } } // 3. 獲取頁面 page, err := manager.GetPage() if err != nil { return fmt.Errorf("failed to get page: %w", err) } // 4. 檢查當前 URL,如果不是 Gemini 則導航 currentURL, _ := page.Info() if !strings.Contains(currentURL.URL, "gemini.google.com") { fmt.Printf("[GeminiWeb] Navigating to Gemini...\n") if err := NavigateToGemini(page); err != nil { return fmt.Errorf("failed to navigate: %w", err) } time.Sleep(2 * time.Second) } // 5. 檢查登入狀態 fmt.Printf("[GeminiWeb] Checking login status...\n") if !IsLoggedIn(page) { fmt.Printf("[GeminiWeb] Not logged in, continuing anyway\n") if p.cfg.GeminiBrowserVisible { fmt.Println("\n========================================") fmt.Println("Browser is open. You can:") fmt.Println("1. Log in to Gemini now") fmt.Println("2. Continue without login") fmt.Println("========================================\n") } } else { fmt.Printf("[GeminiWeb] Logged in\n") } // 6. 等待頁面就緒 if err := WaitForReady(page); err != nil { fmt.Printf("[GeminiWeb] Warning: %v\n", err) } // 7. 建構提示詞 prompt := buildPromptFromMessages(messages) fmt.Printf("[GeminiWeb] Typing prompt (%d chars)...\n", len(prompt)) // 8. 輸入文字 if err := TypeInput(page, prompt); err != nil { return fmt.Errorf("failed to type input: %w", err) } // 9. 發送 fmt.Printf("[GeminiWeb] Sending message...\n") if err := ClickSend(page); err != nil { return fmt.Errorf("failed to send: %w", err) } // 10. 提取回應 fmt.Printf("[GeminiWeb] Waiting for response...\n") response, err := ExtractResponse(page) if err != nil { return fmt.Errorf("failed to extract response: %w", err) } // 11. 串流回調 cb(apitypes.StreamChunk{Type: apitypes.ChunkText, Text: response}) cb(apitypes.StreamChunk{Type: apitypes.ChunkDone, Done: true}) fmt.Printf("[GeminiWeb] Response complete (%d chars)\n", len(response)) return nil } // buildPromptFromMessages 從訊息列表建構提示詞 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 } // RunLogin 執行登入流程(供 gemini-login 命令使用) func RunLogin(cfg config.BridgeConfig, sessionName string) error { if sessionName == "" { sessionName = "default-session" } sessionDir := filepath.Join(cfg.GeminiAccountDir, sessionName) if err := os.MkdirAll(sessionDir, 0755); err != nil { return fmt.Errorf("failed to create session dir: %w", err) } fmt.Printf("Starting browser for login. Session: %s\n", sessionName) fmt.Printf("Session directory: %s\n", sessionDir) fmt.Println("Please log in to your Gemini account in the browser window.") fmt.Println("Press Ctrl+C when you have completed the login...") manager, err := NewBrowserManager(sessionDir, true) // visible=true if err != nil { return fmt.Errorf("failed to create browser manager: %w", err) } if err := manager.Launch(); err != nil { return fmt.Errorf("failed to launch browser: %w", err) } defer manager.Close() page, err := manager.GetPage() if err != nil { return fmt.Errorf("failed to get page: %w", err) } if err := NavigateToGemini(page); err != nil { return fmt.Errorf("failed to navigate: %w", err) } // 等待用戶手動登入... // 使用 Ctrl+C 退出,瀏覽器資料會自動保存在 userDataDir return nil }