geoGo/internal/publisher/toutiao.go

509 lines
13 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 (
"context"
"fmt"
"geo/internal/config"
"log"
"strings"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/proto"
)
type ToutiaoPublisher struct {
*BasePublisher
}
// NewToutiaoPublisher 构造函数
func NewToutiaoPublisher(ctx context.Context, task *TaskParams, cfg *config.Config, logger *log.Logger) PublisherInerface {
return &ToutiaoPublisher{NewBasePublisher(ctx, task, cfg, logger)}
}
func (p *ToutiaoPublisher) CheckLogin() (bool, string) {
p.LogInfo("检查登录状态...")
if err := p.SetupDriver(); err != nil {
return false, fmt.Sprintf("浏览器启动失败: %v", err)
}
defer p.Close()
p.Page.MustNavigate(p.EditorURL)
p.Sleep(2)
p.WaitForPageReady(5)
if p.CheckLoginStatus() {
p.SaveCookies()
return true, "已登录"
}
return false, "未登录"
}
func (p *ToutiaoPublisher) CheckLoginStatus() bool {
currentURL := p.GetCurrentURL()
// 如果在登录页面,未登录
if strings.Contains(currentURL, p.LoginURL) {
return false
}
return true
}
func (p *ToutiaoPublisher) WaitLogin() (bool, string) {
p.LogInfo("开始等待登录...")
if err := p.SetupDriver(); err != nil {
return false, fmt.Sprintf("浏览器启动失败: %v", err)
}
defer p.Close()
// 先尝试访问已登录页面
p.Page.MustNavigate(p.LoginedURL)
p.Sleep(3)
if p.CheckLoginStatus() {
p.SaveCookies()
p.LogInfo("已有登录状态")
return true, "already_logged_in"
}
// 未登录,跳转到登录页
p.Page.MustNavigate(p.LoginURL)
p.WaitForPageReady(5)
p.LogInfo("请扫码登录...")
// 等待登录完成最多120秒
for i := 0; i < 120; i++ {
currentURL := p.GetCurrentURL()
if strings.Contains(currentURL, p.LoginedURL) {
p.SaveCookies()
p.LogInfo("登录成功")
return true, "login_success"
}
time.Sleep(1 * time.Second)
}
return false, "登录超时,请检查网络或账号状态"
}
// closeCloseBtn 关闭页面上的关闭按钮class="close-btn"的svg
func (p *ToutiaoPublisher) closeCloseBtn() {
p.LogInfo("检查并关闭页面上的关闭按钮...")
// 查找所有 class="close-btn" 的元素
closeBtns, err := p.Page.Elements(".close-btn")
if err != nil {
p.LogInfo("查找关闭按钮失败或无关闭按钮")
return
}
if len(closeBtns) == 0 {
p.LogInfo("未找到关闭按钮")
return
}
p.LogInfo(fmt.Sprintf("找到 %d 个关闭按钮,尝试点击...", len(closeBtns)))
for _, btn := range closeBtns {
if btn == nil {
continue
}
// 检查元素是否可见
visible, err := btn.Visible()
if err != nil {
continue
}
if visible {
p.LogInfo("点击关闭按钮...")
// 使用 JavaScript 强制点击
if err := p.JSClick(btn); err != nil {
p.LogInfo(fmt.Sprintf("点击关闭按钮失败: %v", err))
} else {
p.LogInfo("成功点击关闭按钮")
p.SleepMs(500) // 等待弹窗关闭动画
}
}
}
}
// inputTitle 输入标题
func (p *ToutiaoPublisher) inputTitle() error {
p.LogInfo("输入文章标题...")
// 尝试多种选择器查找标题输入框
titleSelectors := []string{
".publish-editor-title textarea",
"#txtTitle",
".title-input textarea",
"textarea[placeholder*='标题']",
}
var titleInput *rod.Element
var err error
for _, selector := range titleSelectors {
titleInput, err = p.WaitForElementVisible(selector, 5)
if err == nil && titleInput != nil {
p.LogInfo(fmt.Sprintf("找到标题输入框: %s", selector))
break
}
}
if titleInput == nil {
return fmt.Errorf("未找到标题输入框")
}
// 点击获取焦点
if err := titleInput.Click(proto.InputMouseButtonLeft, 1); err != nil {
return fmt.Errorf("点击标题框失败: %v", err)
}
p.SleepMs(300)
// 清空输入框
if err := p.ClearInput(titleInput); err != nil {
titleInput.Input("")
}
p.SleepMs(300)
// 输入标题
if err := p.SetInputValue(titleInput, p.Title); err != nil {
titleInput.Input(p.Title)
}
p.LogInfo(fmt.Sprintf("标题已输入: %s", p.Title))
p.SleepMs(500)
return nil
}
// inputContent 通过导入文件输入内容
func (p *ToutiaoPublisher) inputContent() error {
p.LogInfo("开始导入文章内容...")
// 查找所有 class="close-btn" 的元素
p.closeCloseBtn()
p.SleepMs(500)
// 1. 找到并点击导入按钮class="syl-toolbar-button"
p.LogInfo("查找导入按钮...")
p.LogInfo("查找导入按钮...")
// 先找 class 包含 doc-import 的 div
docImportDiv, err := p.WaitForElementVisible("[class*='doc-import']", 10)
if err != nil {
return fmt.Errorf("未找到包含doc-import的元素: %v", err)
}
p.LogInfo("找到包含doc-import的元素开始查找其中的button...")
// 在该 div 下查找 button
importBtn, err := docImportDiv.Element("button")
if err != nil {
// 尝试查找 button 的多种可能选择器
importBtn, err = docImportDiv.Element("button:first-child")
if err != nil {
importBtn, err = docImportDiv.Element(".syl-toolbar-button")
if err != nil {
return fmt.Errorf("在doc-import元素下未找到按钮: %v", err)
}
}
}
if importBtn == nil {
return fmt.Errorf("未找到导入按钮")
}
p.LogInfo("找到导入按钮,准备点击...")
// 点击导入按钮
if err := p.JSClick(importBtn); err != nil {
return fmt.Errorf("点击导入按钮失败: %v", err)
}
p.LogInfo("已点击导入按钮")
p.SleepMs(1000)
// 2. 找到文件上传输入框并上传文件
p.LogInfo("查找文件上传输入框...")
// 文件上传输入框的选择器
fileInputSelectors := []string{
"input[type='file'][accept*='.doc']",
"input[type='file'][accept*='application']",
"input[type='file']",
}
var fileInput *rod.Element
for _, selector := range fileInputSelectors {
fileInput, err = p.Page.Element(selector)
if err == nil && fileInput != nil {
p.LogInfo(fmt.Sprintf("找到文件上传输入框: %s", selector))
break
}
}
if fileInput == nil {
return fmt.Errorf("未找到文件上传输入框")
}
// 检查是否有可用的文件路径
if p.SourcePath == "" && p.ImagePath == "" {
return fmt.Errorf("未提供要导入的文件路径")
}
// 优先使用 SourcePathWord文档路径其次使用 ImagePath
filePath := p.SourcePath
if filePath == "" {
filePath = p.ImagePath
}
p.LogInfo(fmt.Sprintf("开始上传文件: %s", filePath))
// 上传文件
if err := fileInput.SetFiles([]string{filePath}); err != nil {
return fmt.Errorf("上传文件失败: %v", err)
}
p.LogInfo("文件已上传,等待导入处理...")
// 3. 等待导入成功的弹窗出现
p.LogInfo("等待导入成功弹窗...")
// 等待导入成功的提示出现
successSelectors := []string{
".byte-message-success",
".toast-success",
"[class*='success']",
"[class*='导入成功']",
".syl-toast-success",
}
var successMsg *rod.Element
for attempt := 0; attempt < 30; attempt++ {
for _, selector := range successSelectors {
successMsg, err = p.Page.Element(selector)
if err == nil && successMsg != nil {
text, _ := successMsg.Text()
if strings.Contains(text, "成功") || strings.Contains(text, "导入") {
p.LogInfo(fmt.Sprintf("检测到导入成功提示: %s", text))
break
}
}
}
if successMsg != nil {
break
}
p.SleepMs(500)
}
// 等待弹窗消失(导入完成的标志)
p.LogInfo("等待导入完成,弹窗消失...")
time.Sleep(2 * time.Second)
// 等待内容加载完成
p.LogInfo("等待内容加载到编辑器...")
time.Sleep(3 * time.Second)
// 验证内容是否已导入
contentEditor, err := p.WaitForElementVisible(".ProseMirror", 10)
if err == nil {
text, _ := contentEditor.Text()
if len(text) > 0 {
p.LogInfo(fmt.Sprintf("内容导入成功,内容长度: %d", len(text)))
} else {
p.LogWarning("内容可能未正确导入,编辑器为空")
}
} else {
p.LogWarning("未找到内容编辑器,无法验证导入结果")
}
p.LogInfo("文章内容导入完成")
return nil
}
// isDrawerClosed 检查弹窗是否已关闭
func (p *ToutiaoPublisher) isDrawerClosed() bool {
drawerWrappers, err := p.Page.Elements(".byte-drawer-wrapper")
if err != nil {
// 没找到弹窗元素,可能已关闭
return true
}
for _, wrapper := range drawerWrappers {
className, err := wrapper.Attribute("class")
if err == nil && className != nil && strings.Contains(*className, "byte-drawer-wrapper-hide") {
return true
}
}
return false
}
// clickPublish 点击发布按钮
func (p *ToutiaoPublisher) clickPublish() error {
p.LogInfo("点击发布按钮...")
// 查找发布按钮
publishBtn, err := p.WaitForElementClickable(".publish-btn-last, .publish-footer .byte-btn-primary", 5)
if err != nil {
// 尝试其他选择器
publishBtn, err = p.Page.Element("button:contains('预览并发布')")
if err != nil {
return fmt.Errorf("未找到发布按钮: %v", err)
}
}
// 滚动到按钮位置
if err := p.ScrollToElement(publishBtn); err != nil {
p.LogInfo(fmt.Sprintf("滚动到发布按钮失败: %v", err))
}
p.SleepMs(500)
// 点击第一次发布按钮
if err := p.JSClick(publishBtn); err != nil {
return fmt.Errorf("点击发布按钮失败: %v", err)
}
p.LogInfo("已点击第一次发布按钮")
p.SleepMs(2000)
// 第二次点击确认发布
p.LogInfo("查找第二次确认发布按钮...")
secondPublishBtn, err := p.WaitForElementClickable(".publish-btn.publish-btn-last", 5)
if err != nil {
secondPublishBtn, err = p.Page.Element("button:contains('预览并发布')")
if err != nil {
p.LogInfo("未找到第二次发布确认按钮,可能已经发布")
return nil
}
}
if secondPublishBtn != nil {
if err := p.ScrollToElement(secondPublishBtn); err != nil {
p.LogInfo(fmt.Sprintf("滚动到确认按钮失败: %v", err))
}
p.SleepMs(500)
if err := p.JSClick(secondPublishBtn); err != nil {
p.LogInfo(fmt.Sprintf("点击第二次发布确认按钮失败: %v", err))
} else {
p.LogInfo("已点击第二次发布确认按钮")
p.SleepMs(3000)
}
}
return nil
}
// waitForPublishResult 等待发布结果
func (p *ToutiaoPublisher) waitForPublishResult() (bool, string) {
p.LogInfo("等待发布结果...")
for attempt := 0; attempt < p.MaxRetries; attempt++ {
currentURL := p.GetCurrentURL()
p.LogInfo(fmt.Sprintf("第 %d 次检查 - URL: %s", attempt+1, currentURL))
// 检查是否发布成功
if strings.Contains(currentURL, "success") || !strings.Contains(currentURL, "publish") {
p.LogInfo(fmt.Sprintf("发布成功URL: %s", currentURL))
return true, "发布成功"
}
// 检查错误提示
errorSelectors := []string{
"[class*='error']",
"[class*='toast-error']",
".byte-message-error",
}
for _, selector := range errorSelectors {
errorMsgs, err := p.Page.Elements(selector)
if err == nil {
for _, elem := range errorMsgs {
text, _ := elem.Text()
if text != "" && (strings.Contains(text, "失败") || strings.Contains(strings.ToLower(text), "error")) {
p.LogError(fmt.Sprintf("发布失败: %s", text))
return false, fmt.Sprintf("发布失败: %s", text)
}
}
}
}
// 检查成功提示
successSelectors := []string{
"[class*='success']",
".byte-message-success",
}
for _, selector := range successSelectors {
successMsgs, err := p.Page.Elements(selector)
if err == nil {
for _, elem := range successMsgs {
text, _ := elem.Text()
if text != "" && strings.Contains(text, "成功") {
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
return true, text
}
}
}
}
p.SleepMs(p.RetryDelay)
}
p.LogWarning("发布结果未知")
return false, "发布结果未知"
}
// InitPage 初始化页面
func (p *ToutiaoPublisher) InitPage() error {
// 访问发布页面
// 尝试加载cookies并检查登录状态
if err := p.LoadCookies(); err == nil {
p.Page.MustNavigate(p.EditorURL)
p.WaitForPageReady(5)
if p.CheckLoginStatus() {
p.SaveCookies()
return nil
}
}
return fmt.Errorf("需要登录")
}
// PublishNote 发布文章
func (p *ToutiaoPublisher) PublishNote() (bool, string) {
p.StartNote()
// 初始化浏览器
if err := p.SetupDriver(); err != nil {
return false, fmt.Sprintf("浏览器启动失败: %v", err)
}
defer p.Close()
// 执行发布流程
steps := []struct {
name string
fn func() error
}{
{"初始化页面", p.InitPage},
{"输入内容", p.inputContent},
{"输入标题", p.inputTitle},
{"点击发布", p.clickPublish},
}
for _, step := range steps {
if err := step.fn(); err != nil {
// 失败时截图
screenshotFile := fmt.Sprintf("screenshot_%s_%d.png", step.name, time.Now().Unix())
p.Screenshot(screenshotFile)
p.LogStep(step.name, false, err.Error())
return false, fmt.Sprintf("%s失败: %v", step.name, err)
}
p.LogStep(step.name, true, "")
p.SleepMs(500)
}
// 等待发布结果
return p.waitForPublishResult()
}
// LogWarning 记录警告日志
func (p *ToutiaoPublisher) LogWarning(message string) {
p.Logger.Printf("⚠️ %s", message)
}