geoGo/internal/publisher/base.go

317 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}