geoGo/internal/publisher/xiaohongshu.go

426 lines
10 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 (
"fmt"
"strings"
"time"
"geo/internal/config"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/proto"
)
type XiaohongshuPublisher struct {
*BasePublisher
}
func NewXiaohongshuPublisher(headless bool, title, content string, tags []string, tenantID, platIndex, requestID, imagePath, wordPath string, platInfo map[string]interface{}, cfg *config.Config) *XiaohongshuPublisher {
base := NewBasePublisher(headless, title, content, tags, tenantID, platIndex, requestID, imagePath, wordPath, platInfo, cfg)
if platInfo != nil {
base.LoginURL = getString(platInfo, "login_url")
base.EditorURL = getString(platInfo, "edit_url")
base.LoginedURL = getString(platInfo, "logined_url")
}
return &XiaohongshuPublisher{BasePublisher: base}
}
func (p *XiaohongshuPublisher) CheckLoginStatus() bool {
url := p.GetCurrentURL()
// 如果URL包含登录相关关键词表示未登录
if strings.Contains(url, "login") || strings.Contains(url, "signin") || strings.Contains(url, "passport") {
return false
}
// 如果URL是编辑页面或主页表示已登录
if strings.Contains(url, "creator") || strings.Contains(url, "editor") || strings.Contains(url, "publish") {
return true
}
return true
}
func (p *XiaohongshuPublisher) CheckLogin() (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)
p.WaitForPageReady(5)
if p.CheckLoginStatus() {
p.SaveCookies()
return true, "已登录"
}
return false, "未登录"
}
func (p *XiaohongshuPublisher) 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.LogInfo("请扫描二维码登录...")
// 等待登录完成最多120秒
for i := 0; i < 120; i++ {
time.Sleep(1 * time.Second)
if p.CheckLoginStatus() {
p.SaveCookies()
p.LogInfo("登录成功")
return true, "login_success"
}
}
return false, "登录超时"
}
func (p *XiaohongshuPublisher) inputContent() error {
p.LogInfo("输入文章内容...")
// 等待编辑器加载
contentEditor, err := p.WaitForElementVisible(".tiptap.ProseMirror", 10)
if err != nil {
// 尝试其他选择器
contentEditor, err = p.WaitForElementVisible("[contenteditable='true']", 10)
if err != nil {
return fmt.Errorf("未找到内容编辑器: %v", err)
}
}
// 点击获取焦点 - 使用 Click 方法
if err := contentEditor.Click(proto.InputMouseButtonLeft, 1); err != nil {
return fmt.Errorf("点击编辑器失败: %v", err)
}
p.SleepMs(500)
// 清空现有内容 - 使用 JavaScript 清空
if err := p.ClearContentEditable(contentEditor); err != nil {
p.LogInfo(fmt.Sprintf("清空编辑器失败: %v", err))
}
p.SleepMs(300)
// 输入新内容 - 使用 JavaScript 设置内容
if err := p.SetContentEditable(contentEditor, p.Content); err != nil {
// 如果 JS 方式失败,尝试直接输入
contentEditor.Input(p.Content)
}
p.LogInfo(fmt.Sprintf("内容已输入,长度: %d", len(p.Content)))
return nil
}
func (p *XiaohongshuPublisher) inputTitle() error {
p.LogInfo("输入标题...")
// 查找标题输入框
titleSelectors := []string{
"textarea.d-input",
".d-input input",
"textarea[placeholder*='标题']",
"textarea",
}
var titleInput *rod.Element
var err error
for _, selector := range titleSelectors {
titleInput, err = p.WaitForElementVisible(selector, 3)
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(500)
// 清空输入框
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))
return nil
}
func (p *XiaohongshuPublisher) inputTags() error {
if len(p.Tags) == 0 {
p.LogInfo("无标签需要设置")
return nil
}
p.LogInfo(fmt.Sprintf("设置标签: %v", p.Tags))
// 构建标签字符串
tagStr := ""
for _, tag := range p.Tags {
if tagStr != "" {
tagStr += " "
}
tagStr += "#" + tag
}
// 查找标签输入区域
tagInput, err := p.WaitForElementVisible(".tiptap-container [contenteditable='true']", 5)
if err != nil {
p.LogInfo("未找到标签输入框,跳过标签设置")
return nil
}
if err := tagInput.Click(proto.InputMouseButtonLeft, 1); err != nil {
p.LogInfo(fmt.Sprintf("点击标签框失败: %v", err))
}
p.SleepMs(500)
if err := p.SetContentEditable(tagInput, tagStr); err != nil {
tagInput.Input(tagStr)
}
p.LogInfo("标签设置完成")
return nil
}
func (p *XiaohongshuPublisher) uploadImage() error {
if p.ImagePath == "" {
p.LogInfo("无封面图片,跳过上传")
return nil
}
p.LogInfo(fmt.Sprintf("上传封面图片: %s", p.ImagePath))
// 查找封面上传按钮
uploadBtn, err := p.WaitForElementClickable(".upload-content", 5)
if err != nil {
p.LogInfo("未找到封面上传区域,跳过")
return nil
}
// 使用 Click 方法
if err := uploadBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
p.LogInfo(fmt.Sprintf("点击上传按钮失败: %v", err))
}
p.SleepMs(1000)
// 查找文件输入框
fileInput, err := p.Page.Element("input[type='file']")
if err != nil {
return fmt.Errorf("未找到文件输入框: %v", err)
}
// 使用 SetFiles 上传文件
if err := fileInput.SetFiles([]string{p.ImagePath}); err != nil {
return fmt.Errorf("上传图片失败: %v", err)
}
p.LogInfo("图片上传成功")
p.Sleep(3)
return nil
}
func (p *XiaohongshuPublisher) clickPublish() error {
p.LogInfo("点击发布按钮...")
// 滚动到底部
if _, err := p.Page.Eval(`() => window.scrollTo(0, document.body.scrollHeight)`); err != nil {
p.LogInfo(fmt.Sprintf("滚动到底部失败: %v", err))
}
p.SleepMs(1000)
// 查找发布按钮
publishSelectors := []string{
".publish-page-publish-btn button",
".publish-btn",
".submit-btn",
"button[type='submit']",
}
var publishBtn *rod.Element
var err error
for _, selector := range publishSelectors {
publishBtn, err = p.WaitForElementClickable(selector, 5)
if err == nil && publishBtn != nil {
p.LogInfo(fmt.Sprintf("找到发布按钮: %s", selector))
break
}
}
// 如果还是没找到,通过文本查找
if publishBtn == nil {
publishBtn, err = p.Page.ElementX("//button[contains(text(), '发布')]")
if err != nil {
return fmt.Errorf("未找到发布按钮: %v", err)
}
}
// 滚动到按钮位置
if err := p.ScrollToElement(publishBtn); err != nil {
p.LogInfo(fmt.Sprintf("滚动到按钮失败: %v", err))
}
p.SleepMs(500)
// 点击发布 - 使用 Click 方法
if err := publishBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
return fmt.Errorf("点击发布按钮失败: %v", err)
}
p.LogInfo("已点击发布按钮")
return nil
}
func (p *XiaohongshuPublisher) waitForPublishResult() (bool, string) {
p.LogInfo("等待发布结果...")
// 等待最多60秒
for i := 0; i < 60; i++ {
p.SleepMs(1000)
// 检查URL是否跳转到成功页面
currentURL := p.GetCurrentURL()
if strings.Contains(currentURL, "success") ||
strings.Contains(currentURL, "content/manage") ||
strings.Contains(currentURL, "work-management") {
p.LogInfo("发布成功!")
return true, "发布成功"
}
// 检查是否有成功提示
elements, _ := p.Page.Elements(".semi-toast-content, .toast-success, [class*='success']")
for _, el := range elements {
text, _ := el.Text()
if strings.Contains(text, "成功") || strings.Contains(text, "已发布") {
p.LogInfo(fmt.Sprintf("发布成功: %s", text))
return true, text
}
}
// 检查是否有失败提示
elements, _ = p.Page.Elements(".semi-toast-error, .toast-error, [class*='error']")
for _, el := range elements {
text, _ := el.Text()
if strings.Contains(text, "失败") || strings.Contains(text, "错误") {
p.LogError(fmt.Sprintf("发布失败: %s", text))
return false, text
}
}
}
return false, "发布结果未知(超时)"
}
func (p *XiaohongshuPublisher) PublishNote() (bool, string) {
p.LogInfo(strings.Repeat("=", 50))
p.LogInfo("开始发布小红书笔记...")
p.LogInfo(fmt.Sprintf("标题: %s", p.Title))
p.LogInfo(fmt.Sprintf("内容长度: %d", len(p.Content)))
p.LogInfo(fmt.Sprintf("标签: %v", p.Tags))
p.LogInfo(strings.Repeat("=", 50))
// 初始化浏览器
if err := p.SetupDriver(); err != nil {
return false, fmt.Sprintf("浏览器启动失败: %v", err)
}
defer p.Close()
// 访问已登录页面
p.Page.MustNavigate(p.LoginedURL)
p.Sleep(3)
p.WaitForPageReady(5)
// 尝试加载cookies
if err := p.LoadCookies(); err == nil {
p.RefreshPage()
p.Sleep(2)
if p.CheckLoginStatus() {
p.LogInfo("使用cookies登录成功")
} else {
p.LogInfo("cookies已过期需要重新登录")
return false, "需要登录"
}
}
// 检查登录状态
if !p.CheckLoginStatus() {
return false, "需要登录"
}
// 保存cookies
p.SaveCookies()
// 访问发布页面
p.Page.MustNavigate(p.EditorURL)
p.Sleep(3)
p.WaitForPageReady(5)
// 执行发布流程
steps := []struct {
name string
fn func() error
}{
{"输入内容", p.inputContent},
{"输入标题", p.inputTitle},
{"设置标签", p.inputTags},
{"上传封面", p.uploadImage},
}
for _, step := range steps {
if err := step.fn(); err != nil {
p.LogStep(step.name, false, err.Error())
return false, fmt.Sprintf("%s失败: %v", step.name, err)
}
p.LogStep(step.name, true, "")
p.SleepMs(500)
}
// 点击发布
if err := p.clickPublish(); err != nil {
return false, err.Error()
}
// 等待发布结果
return p.waitForPublishResult()
}
func getString(m map[string]interface{}, key string) string {
if v, ok := m[key]; ok {
if s, ok := v.(string); ok {
return s
}
}
return ""
}