package publisher import ( "encoding/json" "fmt" "log" "os" "path/filepath" "time" "geo/internal/config" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" ) type BasePublisher struct { Headless bool Title string Content string Tags []string TenantID string PlatIndex string RequestID string ImagePath string WordPath string Browser *rod.Browser Page *rod.Page Logger *log.Logger LogFile *os.File LoginURL string EditorURL string LoginedURL string CookiesFile string PlatInfo map[string]interface{} config *config.Config } // NewBasePublisher 构造函数,增加 logger 参数 func NewBasePublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, config *config.Config, logger *log.Logger) *BasePublisher { cookiesDir := filepath.Join(config.Sys.CookiesDir, tenantID) os.MkdirAll(cookiesDir, 0755) cookiesFile := filepath.Join(cookiesDir, platIndex+".json") var baseLogger *log.Logger var logFile *os.File if logger != nil { // 使用传入的logger baseLogger = logger logFile = nil } else { // 兼容旧逻辑 logsDir := config.Sys.LogsDir if logsDir == "" { logsDir = "./logs" } os.MkdirAll(logsDir, 0755) logFile, _ = os.Create(filepath.Join(logsDir, requestID+".log")) baseLogger = log.New(logFile, "", log.LstdFlags) } return &BasePublisher{ Headless: headless, Title: title, Content: content, Tags: tags, TenantID: tenantID, PlatIndex: platIndex, RequestID: requestID, ImagePath: imagePath, WordPath: wordPath, Logger: baseLogger, LogFile: logFile, CookiesFile: cookiesFile, PlatInfo: platInfo, config: config, } } func (b *BasePublisher) SetupDriver() error { l := launcher.New() l.Headless(b.Headless) // 设置用户数据目录 userDataDir := filepath.Join(b.config.Sys.ChromeDataDir, b.TenantID) os.MkdirAll(userDataDir, 0755) l.UserDataDir(userDataDir) // 设置 Leakless 模式(解决 Windows 上的问题) l.Leakless(false) // 设置 Chrome 启动参数 l.Set("disable-blink-features", "AutomationControlled") l.Set("no-sandbox") l.Set("disable-dev-shm-usage") l.Set("disable-gpu") l.Set("disable-software-rasterizer") l.Set("disable-setuid-sandbox") l.Set("remote-debugging-port", "9222") // 窗口大小 l.Set("window-size", "1920,1080") l.Set("lang", "zh-CN") l.Set("start-maximized") l.Set("force-device-scale-factor", "1") url, err := l.Launch() if err != nil { return fmt.Errorf("启动浏览器失败: %v", err) } b.Browser = rod.New().ControlURL(url).MustConnect() b.Page = b.Browser.MustPage() // 删除这行!!!! // b.Page.MustSetViewport(1920, 1080, 1, false) return nil } func (b *BasePublisher) Close() { if b.Page != nil { b.Page.Close() } if b.Browser != nil { b.Browser.Close() } if b.LogFile != nil { b.LogFile.Close() } } func (b *BasePublisher) 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) } func (b *BasePublisher) 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) } func (b *BasePublisher) RefreshPage() error { _, err := b.Page.Eval(`() => location.reload()`) return err } func (b *BasePublisher) WaitForPageReady(timeout int) error { return b.Page.Timeout(time.Duration(timeout) * time.Second).WaitLoad() } func (b *BasePublisher) WaitForElement(selector string, timeout int) (*rod.Element, error) { return b.Page.Timeout(time.Duration(timeout) * time.Second).Element(selector) } func (b *BasePublisher) 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 } func (b *BasePublisher) WaitForElementClickable(selector string, timeout int) (*rod.Element, error) { el, err := b.WaitForElementVisible(selector, timeout) if err != nil { return nil, err } if err := el.WaitVisible(); err != nil { return nil, err } return el, nil } func (b *BasePublisher) JSClick(element *rod.Element) error { if element == nil { return fmt.Errorf("element is nil") } // 方法1:使用 rod 自带的 Click return element.Click(proto.InputMouseButtonLeft, 1) //// 方法2:使用 JavaScript 点击(修复版) //_, err := element.Evaluate(&rod.EvalOptions{ // JS: `function(el) { // if(el && el.click) { // el.click(); // return true; // } // return false; // }(this)`, //}) //return err } func (b *BasePublisher) ScrollToElement(element *rod.Element) error { _, err := element.Evaluate(&rod.EvalOptions{ JS: `el => el.scrollIntoView({block: 'center', behavior: 'smooth'})`, }) return err } func (b *BasePublisher) Sleep(seconds int) { time.Sleep(time.Duration(seconds) * time.Second) } func (b *BasePublisher) LogStep(stepName string, success bool, message string) { if success { b.Logger.Printf("✅ %s: 成功 %s", stepName, message) } else { b.Logger.Printf("❌ %s: 失败 %s", stepName, message) } } func (b *BasePublisher) LogInfo(message string) { b.Logger.Printf("📌 %s", message) } func (b *BasePublisher) LogError(message string) { b.Logger.Printf("❌ %s", message) } func (b *BasePublisher) GetCurrentURL() string { info := b.Page.MustInfo() return info.URL } func (b *BasePublisher) Screenshot(filename string) error { data, err := b.Page.Screenshot(false, nil) if err != nil { return err } return os.WriteFile(filename, data, 0644) } // 抽象方法 - 子类需要实现 func (b *BasePublisher) WaitLogin() (bool, string) { return false, "需要实现" } func (b *BasePublisher) CheckLoginStatus() bool { return false } func (b *BasePublisher) CheckLogin() (bool, string) { return false, "需要实现" } func (b *BasePublisher) PublishNote() (bool, string) { return false, "需要实现" } // ClearContentEditable 清空 contenteditable 元素的内容 func (b *BasePublisher) ClearContentEditable(element *rod.Element) error { _, err := element.Evaluate(&rod.EvalOptions{ JS: `el => { el.innerText = ''; el.innerHTML = ''; el.dispatchEvent(new Event('input', {bubbles: true})); }`, }) return err } // SetContentEditable 设置 contenteditable 元素的内容 func (b *BasePublisher) SetContentEditable(element *rod.Element, content string) error { _, err := element.Evaluate(&rod.EvalOptions{ JS: `(el, val) => { el.innerText = val; el.dispatchEvent(new Event('input', {bubbles: true})); }`, JSArgs: []interface{}{content}, }) return err } // SetInputValue 设置输入框值并触发事件 func (b *BasePublisher) 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 *BasePublisher) ClearInput(element *rod.Element) error { _, err := element.Evaluate(&rod.EvalOptions{ JS: `el => { el.value = ''; el.dispatchEvent(new Event('input', {bubbles: true})); }`, }) return err } // SleepMs 毫秒级等待 func (b *BasePublisher) SleepMs(milliseconds int) { time.Sleep(time.Duration(milliseconds) * time.Millisecond) }