283 lines
6.9 KiB
Go
283 lines
6.9 KiB
Go
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
|
|
}
|
|
|
|
func NewBasePublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, config *config.Config) *BasePublisher {
|
|
cookiesDir := filepath.Join(config.Sys.CookiesDir, tenantID)
|
|
os.MkdirAll(cookiesDir, 0755)
|
|
cookiesFile := filepath.Join(cookiesDir, platIndex+".json")
|
|
|
|
logFile, _ := os.Create(filepath.Join(config.Sys.LogsDir, requestID+".log"))
|
|
logger := 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: logger,
|
|
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")
|
|
|
|
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 {
|
|
// 使用 element.Evaluate 并传入 EvalOptions
|
|
_, err := element.Evaluate(&rod.EvalOptions{
|
|
JS: `el => el.click()`,
|
|
})
|
|
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)
|
|
}
|