2026-04-02 14:45:41 +00:00
|
|
|
|
package geminiweb
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"cursor-api-proxy/internal/apitypes"
|
|
|
|
|
|
"cursor-api-proxy/internal/config"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"os"
|
2026-04-02 17:05:54 +00:00
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"sync"
|
2026-04-02 14:45:41 +00:00
|
|
|
|
"time"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// Provider 使用持久化瀏覽器管理器
|
2026-04-02 14:45:41 +00:00
|
|
|
|
type Provider struct {
|
2026-04-02 17:05:54 +00:00
|
|
|
|
cfg config.BridgeConfig
|
|
|
|
|
|
managerOnce sync.Once
|
|
|
|
|
|
manager *BrowserManager
|
|
|
|
|
|
managerErr error
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// NewProvider 建立新的 Provider
|
2026-04-02 14:45:41 +00:00
|
|
|
|
func NewProvider(cfg config.BridgeConfig) *Provider {
|
|
|
|
|
|
return &Provider{cfg: cfg}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// getName 返回 Provider 名稱
|
2026-04-02 14:45:41 +00:00
|
|
|
|
func (p *Provider) Name() string {
|
|
|
|
|
|
return "gemini-web"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// Close 關閉瀏覽器
|
2026-04-02 14:45:41 +00:00
|
|
|
|
func (p *Provider) Close() error {
|
2026-04-02 17:05:54 +00:00
|
|
|
|
if p.manager != nil {
|
|
|
|
|
|
return p.manager.Close()
|
|
|
|
|
|
}
|
2026-04-02 14:45:41 +00:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// 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
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// getSessionDir 獲取 session 目錄
|
|
|
|
|
|
func (p *Provider) getSessionDir() string {
|
|
|
|
|
|
// 使用單一 session 目錄(簡化設計)
|
|
|
|
|
|
return filepath.Join(p.cfg.GeminiAccountDir, "default-session")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Generate 生成回應
|
2026-04-02 14:45:41 +00:00
|
|
|
|
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 17:05:54 +00:00
|
|
|
|
// 1. 獲取瀏覽器管理器
|
|
|
|
|
|
manager, err := p.getManager()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("failed to get browser manager: %w", err)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// 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)
|
2026-04-02 16:45:59 +00:00
|
|
|
|
}
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// 3. 獲取頁面
|
|
|
|
|
|
page, err := manager.GetPage()
|
2026-04-02 14:45:41 +00:00
|
|
|
|
if err != nil {
|
2026-04-02 17:05:54 +00:00
|
|
|
|
return fmt.Errorf("failed to get page: %w", err)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// 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)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
2026-04-02 17:05:54 +00:00
|
|
|
|
time.Sleep(2 * time.Second)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// 5. 檢查登入狀態
|
2026-04-02 16:40:57 +00:00
|
|
|
|
fmt.Printf("[GeminiWeb] Checking login status...\n")
|
2026-04-02 17:05:54 +00:00
|
|
|
|
if !IsLoggedIn(page) {
|
|
|
|
|
|
fmt.Printf("[GeminiWeb] Not logged in, continuing anyway\n")
|
2026-04-02 16:45:59 +00:00
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
if p.cfg.GeminiBrowserVisible {
|
2026-04-02 16:45:59 +00:00
|
|
|
|
fmt.Println("\n========================================")
|
2026-04-02 16:51:55 +00:00
|
|
|
|
fmt.Println("Browser is open. You can:")
|
2026-04-02 17:05:54 +00:00
|
|
|
|
fmt.Println("1. Log in to Gemini now")
|
2026-04-02 16:51:55 +00:00
|
|
|
|
fmt.Println("2. Continue without login")
|
2026-04-02 16:45:59 +00:00
|
|
|
|
fmt.Println("========================================\n")
|
|
|
|
|
|
}
|
2026-04-02 17:05:54 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
fmt.Printf("[GeminiWeb] Logged in\n")
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// 6. 等待頁面就緒
|
|
|
|
|
|
if err := WaitForReady(page); err != nil {
|
|
|
|
|
|
fmt.Printf("[GeminiWeb] Warning: %v\n", err)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// 7. 建構提示詞
|
2026-04-02 14:45:41 +00:00
|
|
|
|
prompt := buildPromptFromMessages(messages)
|
2026-04-02 17:05:54 +00:00
|
|
|
|
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
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// buildPromptFromMessages 從訊息列表建構提示詞
|
2026-04-02 14:45:41 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// RunLogin 執行登入流程(供 gemini-login 命令使用)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
func RunLogin(cfg config.BridgeConfig, sessionName string) error {
|
|
|
|
|
|
if sessionName == "" {
|
2026-04-02 17:05:54 +00:00
|
|
|
|
sessionName = "default-session"
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
sessionDir := filepath.Join(cfg.GeminiAccountDir, sessionName)
|
|
|
|
|
|
if err := os.MkdirAll(sessionDir, 0755); err != nil {
|
|
|
|
|
|
return fmt.Errorf("failed to create session dir: %w", err)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Starting browser for login. Session: %s\n", sessionName)
|
2026-04-02 17:05:54 +00:00
|
|
|
|
fmt.Printf("Session directory: %s\n", sessionDir)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
fmt.Println("Please log in to your Gemini account in the browser window.")
|
|
|
|
|
|
fmt.Println("Press Ctrl+C when you have completed the login...")
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
manager, err := NewBrowserManager(sessionDir, true) // visible=true
|
2026-04-02 14:45:41 +00:00
|
|
|
|
if err != nil {
|
2026-04-02 17:05:54 +00:00
|
|
|
|
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)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
2026-04-02 17:05:54 +00:00
|
|
|
|
defer manager.Close()
|
2026-04-02 14:45:41 +00:00
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
page, err := manager.GetPage()
|
2026-04-02 14:45:41 +00:00
|
|
|
|
if err != nil {
|
2026-04-02 17:05:54 +00:00
|
|
|
|
return fmt.Errorf("failed to get page: %w", err)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := NavigateToGemini(page); err != nil {
|
|
|
|
|
|
return fmt.Errorf("failed to navigate: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// 等待用戶手動登入...
|
|
|
|
|
|
// 使用 Ctrl+C 退出,瀏覽器資料會自動保存在 userDataDir
|
2026-04-02 14:45:41 +00:00
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|