2026-04-02 14:45:41 +00:00
|
|
|
|
package geminiweb
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/go-rod/rod"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const geminiURL = "https://gemini.google.com/app"
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// 輸入框選擇器(依優先順序)
|
|
|
|
|
|
var inputSelectors = []string{
|
|
|
|
|
|
".ProseMirror",
|
|
|
|
|
|
"rich-textarea",
|
|
|
|
|
|
"div[role='textbox'][contenteditable='true']",
|
|
|
|
|
|
"div[contenteditable='true']",
|
|
|
|
|
|
"textarea",
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// NavigateToGemini 導航到 Gemini
|
2026-04-02 14:45:41 +00:00
|
|
|
|
func NavigateToGemini(page *rod.Page) error {
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
|
|
if err := page.Context(ctx).Navigate(geminiURL); err != nil {
|
2026-04-02 17:05:54 +00:00
|
|
|
|
return fmt.Errorf("failed to navigate: %w", err)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
return page.Context(ctx).WaitLoad()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// IsLoggedIn 檢查是否已登入
|
2026-04-02 14:45:41 +00:00
|
|
|
|
func IsLoggedIn(page *rod.Page) bool {
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
for _, sel := range inputSelectors {
|
|
|
|
|
|
if _, err := page.Context(ctx).Element(sel); err == nil {
|
2026-04-02 16:40:57 +00:00
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// SelectModel 選擇模型(可選)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
func SelectModel(page *rod.Page, model string) error {
|
2026-04-02 17:05:54 +00:00
|
|
|
|
fmt.Printf("[GeminiWeb] Model selection skipped (using current model)\n")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2026-04-02 14:45:41 +00:00
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// TypeInput 在輸入框中輸入文字
|
|
|
|
|
|
func TypeInput(page *rod.Page, text string) error {
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
2026-04-02 17:11:02 +00:00
|
|
|
|
fmt.Println("[GeminiWeb] Looking for input field...")
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 嘗試所有選擇器
|
2026-04-02 17:05:54 +00:00
|
|
|
|
var inputEl *rod.Element
|
2026-04-02 16:40:57 +00:00
|
|
|
|
var err error
|
2026-04-02 17:11:02 +00:00
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
for _, sel := range inputSelectors {
|
2026-04-02 17:11:02 +00:00
|
|
|
|
fmt.Printf(" Trying: %s\n", sel)
|
2026-04-02 17:05:54 +00:00
|
|
|
|
inputEl, err = page.Context(ctx).Element(sel)
|
2026-04-02 16:40:57 +00:00
|
|
|
|
if err == nil {
|
2026-04-02 17:11:02 +00:00
|
|
|
|
fmt.Printf(" ✓ Found with: %s\n", sel)
|
2026-04-02 16:40:57 +00:00
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 14:45:41 +00:00
|
|
|
|
if err != nil {
|
2026-04-02 17:11:02 +00:00
|
|
|
|
// 2. Fallback: 嘗試等待頁面載入完成後重試
|
|
|
|
|
|
fmt.Println("[GeminiWeb] Waiting for page to fully load...")
|
|
|
|
|
|
time.Sleep(3 * time.Second)
|
|
|
|
|
|
|
|
|
|
|
|
for _, sel := range inputSelectors {
|
|
|
|
|
|
fmt.Printf(" Retrying: %s\n", sel)
|
|
|
|
|
|
inputEl, err = page.Context(ctx).Element(sel)
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
fmt.Printf(" ✓ Found with: %s\n", sel)
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
// 3. Debug: 印出頁面標題和 URL
|
|
|
|
|
|
info, _ := page.Info()
|
|
|
|
|
|
fmt.Printf("[GeminiWeb] DEBUG: URL=%s Title=%s\n", info.URL, info.Title)
|
|
|
|
|
|
|
|
|
|
|
|
// 4. Fallback: 嘗試更通用的選擇器
|
|
|
|
|
|
fmt.Println("[GeminiWeb] Trying generic selectors...")
|
|
|
|
|
|
genericSelectors := []string{
|
|
|
|
|
|
"div[contenteditable]",
|
|
|
|
|
|
"[contenteditable]",
|
|
|
|
|
|
"textarea",
|
|
|
|
|
|
"input[type='text']",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, sel := range genericSelectors {
|
|
|
|
|
|
fmt.Printf(" Trying generic: %s\n", sel)
|
|
|
|
|
|
inputEl, err = page.Context(ctx).Element(sel)
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
fmt.Printf(" ✓ Found with: %s\n", sel)
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
info, _ := page.Info()
|
|
|
|
|
|
return fmt.Errorf("input field not found after trying all selectors (URL=%s)", info.URL)
|
2026-04-02 16:40:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// 2. Focus 輸入框
|
2026-04-02 17:11:02 +00:00
|
|
|
|
fmt.Printf("[GeminiWeb] Focusing input field...\n")
|
2026-04-02 17:05:54 +00:00
|
|
|
|
if err := inputEl.Focus(); err != nil {
|
|
|
|
|
|
return fmt.Errorf("failed to focus input: %w", err)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:11:02 +00:00
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
|
2026-04-02 17:11:02 +00:00
|
|
|
|
// 3. 使用 Input 方法
|
|
|
|
|
|
fmt.Printf("[GeminiWeb] Typing %d chars...\n", len(text))
|
2026-04-02 17:05:54 +00:00
|
|
|
|
if err := inputEl.Input(text); err != nil {
|
|
|
|
|
|
return fmt.Errorf("failed to input text: %w", err)
|
2026-04-02 16:40:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:11:02 +00:00
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
2026-04-02 14:45:41 +00:00
|
|
|
|
|
2026-04-02 17:11:02 +00:00
|
|
|
|
fmt.Println("[GeminiWeb] Input complete")
|
2026-04-02 17:05:54 +00:00
|
|
|
|
return nil
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// ClickSend 發送訊息
|
|
|
|
|
|
func ClickSend(page *rod.Page) error {
|
|
|
|
|
|
// 方法 1: 按 Enter
|
|
|
|
|
|
if err := page.Keyboard.Press('\r'); err != nil {
|
|
|
|
|
|
return fmt.Errorf("failed to press Enter: %w", err)
|
2026-04-02 16:40:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2026-04-02 14:45:41 +00:00
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// WaitForReady 等待頁面空閒
|
|
|
|
|
|
func WaitForReady(page *rod.Page) error {
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
|
|
|
|
defer cancel()
|
2026-04-02 14:45:41 +00:00
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
fmt.Println("[GeminiWeb] Checking if page is ready...")
|
2026-04-02 16:40:57 +00:00
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
|
fmt.Println("[GeminiWeb] Page ready check timeout, proceeding anyway")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
default:
|
|
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
|
|
|
|
|
|
|
|
// 檢查是否有停止按鈕
|
|
|
|
|
|
hasStopBtn := false
|
|
|
|
|
|
stopBtns, _ := page.Elements("button[aria-label*='Stop'], button[aria-label*='停止']")
|
|
|
|
|
|
for _, btn := range stopBtns {
|
|
|
|
|
|
visible, _ := btn.Visible()
|
|
|
|
|
|
if visible {
|
|
|
|
|
|
hasStopBtn = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
2026-04-02 16:51:55 +00:00
|
|
|
|
}
|
2026-04-02 16:40:57 +00:00
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
if !hasStopBtn {
|
|
|
|
|
|
fmt.Println("[GeminiWeb] Page is ready")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2026-04-02 16:51:55 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// ExtractResponse 提取回應文字
|
|
|
|
|
|
func ExtractResponse(page *rod.Page) (string, error) {
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
|
|
|
|
|
defer cancel()
|
2026-04-02 14:45:41 +00:00
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
var lastText string
|
|
|
|
|
|
lastUpdate := time.Now()
|
2026-04-02 14:45:41 +00:00
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
|
select {
|
2026-04-02 17:05:54 +00:00
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
|
if lastText != "" {
|
|
|
|
|
|
return lastText, nil
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
2026-04-02 17:05:54 +00:00
|
|
|
|
return "", fmt.Errorf("response timeout")
|
|
|
|
|
|
default:
|
|
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
|
|
|
|
|
|
|
|
// 尋找回應文字
|
|
|
|
|
|
for _, sel := range responseSelectors {
|
|
|
|
|
|
elements, err := page.Elements(sel)
|
|
|
|
|
|
if err != nil || len(elements) == 0 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-04-02 14:45:41 +00:00
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// 取得最後一個元素的文字
|
|
|
|
|
|
lastEl := elements[len(elements)-1]
|
|
|
|
|
|
text, err := lastEl.Text()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
text = strings.TrimSpace(text)
|
|
|
|
|
|
if text != "" && text != lastText && len(text) > len(lastText) {
|
|
|
|
|
|
lastText = text
|
|
|
|
|
|
lastUpdate = time.Now()
|
|
|
|
|
|
fmt.Printf("[GeminiWeb] Response length: %d\n", len(text))
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// 檢查是否已完成(2 秒內沒有新內容)
|
|
|
|
|
|
if time.Since(lastUpdate) > 2*time.Second && lastText != "" {
|
|
|
|
|
|
// 最後檢查一次是否還有停止按鈕
|
|
|
|
|
|
hasStopBtn := false
|
|
|
|
|
|
stopBtns, _ := page.Elements("button[aria-label*='Stop'], button[aria-label*='停止']")
|
|
|
|
|
|
for _, btn := range stopBtns {
|
|
|
|
|
|
visible, _ := btn.Visible()
|
|
|
|
|
|
if visible {
|
|
|
|
|
|
hasStopBtn = true
|
|
|
|
|
|
break
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
if !hasStopBtn {
|
|
|
|
|
|
return lastText, nil
|
|
|
|
|
|
}
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 17:05:54 +00:00
|
|
|
|
// 默認的回應選擇器
|
|
|
|
|
|
var responseSelectors = []string{
|
|
|
|
|
|
".model-response-text",
|
|
|
|
|
|
".message-content",
|
|
|
|
|
|
".markdown",
|
|
|
|
|
|
".prose",
|
|
|
|
|
|
"model-response",
|
2026-04-02 14:45:41 +00:00
|
|
|
|
}
|