package collect import ( "context" "encoding/json" "fmt" "geo/internal/config" "os" "path/filepath" "time" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" "github.com/gofiber/fiber/v2/log" ) // BaseCollector 基础收集器结构 type BaseCollector struct { ctx context.Context Headless bool RequestID string Platform string KeyWords []string Browser *rod.Browser Page *rod.Page Logger log.AllLogger LoginURL string ChatURL string CookiesFile string config *config.Config MaxRetries int RetryDelay int } // NewBaseCollector 构造函数 - 接收已有的 browser 和 page func NewBaseCollector(ctx context.Context, params *CollectParams, config *config.Config, logger log.AllLogger, browser *rod.Browser, page *rod.Page) *BaseCollector { var baseLogger log.AllLogger if logger != nil { baseLogger = logger } else { baseLogger = log.DefaultLogger() } base := &BaseCollector{ ctx: ctx, Headless: false, // 强制有头模式 RequestID: params.RequestID, Platform: params.Platform, KeyWords: params.KeyWords, Logger: baseLogger, config: config, Browser: browser, Page: page, MaxRetries: 3, RetryDelay: 200, } // Cookie文件按平台区分,而不是按用户索引 base.CookiesFile = filepath.Join(base.cookiesDir(), params.Platform+".json") return base } // SetupDriver 初始化浏览器驱动 - 在常驻浏览器模式下,此方法不再需要创建浏览器 // 保留此方法仅为兼容性,实际不做任何操作 func (b *BaseCollector) SetupDriver() error { // 在常驻浏览器模式下,Browser 和 Page 已由 Manager 创建 if b.Browser == nil || b.Page == nil { return fmt.Errorf("Browser 或 Page 未初始化") } return nil } // Close 关闭页面 - 如果是临时 Page,则关闭它 func (b *BaseCollector) Close() { if b.Page != nil { b.Page.MustClose() b.Page = nil b.LogInfo("🔒 临时 Page 已关闭") } } // SaveCookies 保存cookies func (b *BaseCollector) SaveCookies() error { cookies, err := b.Page.Cookies(nil) if err != nil { return err } data, err := json.Marshal(cookies) if err != nil { return err } return os.WriteFile(b.CookiesFile, data, 0644) } // LoadCookies 加载cookies func (b *BaseCollector) LoadCookies() error { data, err := os.ReadFile(b.CookiesFile) if err != nil { return err } var cookies []*proto.NetworkCookieParam if err := json.Unmarshal(data, &cookies); err != nil { return err } return b.Page.SetCookies(cookies) } // DelCookies 删除cookies func (b *BaseCollector) DelCookies() error { return os.Remove(b.CookiesFile) } // WaitForPageReady 等待页面加载完成 func (b *BaseCollector) WaitForPageReady(timeout int) error { return b.Page.Context(b.ctx).WaitLoad() } // WaitForElement 等待元素出现 func (b *BaseCollector) WaitForElement(selector string, timeout int) (*rod.Element, error) { return b.Page.Context(b.ctx).Timeout(time.Duration(timeout) * time.Second).Element(selector) } // WaitForElementVisible 等待元素可见 func (b *BaseCollector) WaitForElementVisible(selector string, timeout int) (*rod.Element, error) { el, err := b.WaitForElement(selector, timeout) if err != nil { return nil, err } if err := el.WaitVisible(); err != nil { return nil, err } return el, nil } // WaitForElementClickable 等待元素可点击 func (b *BaseCollector) WaitForElementClickable(selector string, timeout int) (*rod.Element, error) { el, err := b.WaitForElementVisible(selector, timeout) if err != nil { return nil, err } if err := el.WaitEnabled(); err != nil { return nil, err } return el, nil } // JSClick JavaScript点击元素 func (b *BaseCollector) JSClick(element *rod.Element) error { if element == nil { b.Logger.Warn("element is nil") return fmt.Errorf("element is nil") } err := element.Click(proto.InputMouseButtonLeft, 1) if err != nil { b.Logger.Errorf("click fail: %v", err) } return err } // SetInputValue 设置输入框值 func (b *BaseCollector) SetInputValue(element *rod.Element, value string) error { _, err := element.Evaluate(&rod.EvalOptions{ JS: `(el, val) => { el.value = val; el.dispatchEvent(new Event('input', {bubbles: true})); el.dispatchEvent(new Event('change', {bubbles: true})); }`, JSArgs: []interface{}{value}, }) return err } // ClearInput 清空输入框 func (b *BaseCollector) ClearInput(element *rod.Element) error { _, err := element.Evaluate(&rod.EvalOptions{ JS: `el => { el.value = ''; el.dispatchEvent(new Event('input', {bubbles: true})); }`, }) return err } // Sleep 等待指定秒数 func (b *BaseCollector) Sleep(seconds int) { time.Sleep(time.Duration(seconds) * time.Second) } // SleepMs 等待指定毫秒数 func (b *BaseCollector) SleepMs(milliseconds int) { time.Sleep(time.Duration(milliseconds) * time.Millisecond) } // LogInfo 记录信息日志 func (b *BaseCollector) LogInfo(message string) { b.Logger.Infof("📌 %s", message) } // LogInfof 格式化记录信息日志 func (b *BaseCollector) LogInfof(format string, args ...interface{}) { b.Logger.Infof("📌 "+format, args...) } // LogError 记录错误日志 func (b *BaseCollector) LogError(message string) { b.Logger.Errorf("❌ %s", message) } // LogStep 记录步骤日志 func (b *BaseCollector) LogStep(stepName string, success bool, message string) { if success { b.Logger.Infof("✅ %s: 成功 %s", stepName, message) } else { b.Logger.Errorf("❌ %s: 失败 %s", stepName, message) } } // GetCurrentURL 获取当前URL func (b *BaseCollector) GetCurrentURL() string { info := b.Page.MustInfo() return info.URL } // Screenshot 截图 func (b *BaseCollector) Screenshot(filename string) error { data, err := b.Page.Screenshot(false, nil) if err != nil { return err } return os.WriteFile(filename, data, 0644) } // CheckLoginStatus 检查登录状态(需要子类实现) func (b *BaseCollector) CheckLoginStatus() bool { return false } // WaitLogin 等待登录(需要子类实现) func (b *BaseCollector) WaitLogin() (bool, string) { return false, "需要实现" } // AskQuestion 提问并获取答案(需要子类实现) func (b *BaseCollector) AskQuestion(question string) (*CollectResult, error) { return nil, fmt.Errorf("需要实现") } // InitPage 初始化页面 - 如果 Page 为 nil,则创建临时 Page func (b *BaseCollector) InitPage() error { // 如果 Page 为 nil,创建临时 Page if b.Page == nil { b.Page = b.Browser.MustPage() b.LogInfo("✅ 创建临时 Page") } // 尝试加载cookies if err := b.LoadCookies(); err == nil { b.Page.MustNavigate(b.ChatURL) b.WaitForPageReady(5) } else { // 如果没有 cookies,直接导航到聊天页面 b.Page.MustNavigate(b.ChatURL) b.WaitForPageReady(5) } b.SaveCookies() return nil } // SafeElement 安全地获取元素 func (b *BaseCollector) SafeElement(selector string) (*rod.Element, error) { exists, _, err := b.Page.Has(selector) if err != nil { return nil, err } if !exists { return nil, nil } return b.Page.Element(selector) } // cookiesDir 获取cookie目录 - 按平台区分 func (b *BaseCollector) cookiesDir() string { dir := filepath.Join(b.config.Sys.CookiesDir, b.Platform) os.MkdirAll(dir, 0755) return dir }