Files
xiaohongshu-mcp/xiaohongshu/publish_video.go
zy 8a7fe21b4b Merge pull request #430 from prehisle/fix/publish-tag-popup-stuck
fix: avoid tag popup blocking after content input
2026-03-01 01:19:48 +08:00

189 lines
5.4 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 xiaohongshu
import (
"context"
"log/slog"
"os"
"strings"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/proto"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// PublishVideoContent 发布视频内容
type PublishVideoContent struct {
Title string
Content string
Tags []string
VideoPath string
ScheduleTime *time.Time // 定时发布时间nil 表示立即发布
Visibility string // 可见范围: "公开可见"(默认), "仅自己可见", "仅互关好友可见"
}
// NewPublishVideoAction 进入发布页并切换到"上传视频"
func NewPublishVideoAction(page *rod.Page) (*PublishAction, error) {
pp := page.Timeout(300 * time.Second)
if err := pp.Navigate(urlOfPublic); err != nil {
return nil, errors.Wrap(err, "导航到发布页面失败")
}
// 使用 WaitLoad 代替 WaitIdle更宽松
if err := pp.WaitLoad(); err != nil {
logrus.Warnf("等待页面加载出现问题: %v继续尝试", err)
}
time.Sleep(2 * time.Second)
if err := pp.WaitDOMStable(time.Second, 0.1); err != nil {
logrus.Warnf("等待 DOM 稳定出现问题: %v继续尝试", err)
}
time.Sleep(1 * time.Second)
if err := mustClickPublishTab(pp, "上传视频"); err != nil {
return nil, errors.Wrap(err, "切换到上传视频失败")
}
time.Sleep(1 * time.Second)
return &PublishAction{page: pp}, nil
}
// PublishVideo 上传视频并提交
func (p *PublishAction) PublishVideo(ctx context.Context, content PublishVideoContent) error {
if content.VideoPath == "" {
return errors.New("视频不能为空")
}
page := p.page.Context(ctx)
if err := uploadVideo(page, content.VideoPath); err != nil {
return errors.Wrap(err, "小红书上传视频失败")
}
if err := submitPublishVideo(page, content.Title, content.Content, content.Tags, content.ScheduleTime, content.Visibility); err != nil {
return errors.Wrap(err, "小红书发布失败")
}
return nil
}
// uploadVideo 上传单个本地视频
func uploadVideo(page *rod.Page, videoPath string) error {
pp := page.Timeout(5 * time.Minute) // 视频处理耗时更长
if _, err := os.Stat(videoPath); os.IsNotExist(err) {
return errors.Wrapf(err, "视频文件不存在: %s", videoPath)
}
// 寻找文件上传输入框(与图文一致的 class或退回到 input[type=file]
var fileInput *rod.Element
var err error
fileInput, err = pp.Element(".upload-input")
if err != nil || fileInput == nil {
fileInput, err = pp.Element("input[type='file']")
if err != nil || fileInput == nil {
return errors.New("未找到视频上传输入框")
}
}
fileInput.MustSetFiles(videoPath)
// 对于视频,等待发布按钮变为可点击即表示处理完成
btn, err := waitForPublishButtonClickable(pp)
if err != nil {
return err
}
slog.Info("视频上传/处理完成,发布按钮可点击", "btn", btn)
return nil
}
// waitForPublishButtonClickable 等待发布按钮可点击
func waitForPublishButtonClickable(page *rod.Page) (*rod.Element, error) {
maxWait := 10 * time.Minute
interval := 1 * time.Second
start := time.Now()
selector := ".publish-page-publish-btn button.bg-red"
slog.Info("开始等待发布按钮可点击(视频)")
for time.Since(start) < maxWait {
btn, err := page.Element(selector)
if err == nil && btn != nil {
// 可见性
vis, verr := btn.Visible()
if verr == nil && vis {
// 检查 disabled 属性
if disabled, _ := btn.Attribute("disabled"); disabled == nil {
// 再通过 class 名粗略判断不在禁用态
if cls, _ := btn.Attribute("class"); cls != nil && !strings.Contains(*cls, "disabled") {
return btn, nil
}
// 即使 class 包含 disabled只要没有 disabled 属性,也尝试点击一次以确认
return btn, nil
}
}
}
time.Sleep(interval)
}
return nil, errors.New("等待发布按钮可点击超时")
}
// submitPublishVideo 填写标题、正文、标签并点击发布(等待按钮可点击后再提交)
func submitPublishVideo(page *rod.Page, title, content string, tags []string, scheduleTime *time.Time, visibility string) error {
// 标题
titleElem, err := page.Element("div.d-input input")
if err != nil {
return errors.Wrap(err, "查找标题输入框失败")
}
if err := titleElem.Input(title); err != nil {
return errors.Wrap(err, "输入标题失败")
}
time.Sleep(1 * time.Second)
// 正文 + 标签
contentElem, ok := getContentElement(page)
if !ok {
return errors.New("没有找到内容输入框")
}
if err := contentElem.Input(content); err != nil {
return errors.Wrap(err, "输入正文失败")
}
if err := waitAndClickTitleInput(titleElem); err != nil {
return err
}
if err := inputTags(contentElem, tags); err != nil {
return err
}
time.Sleep(1 * time.Second)
// 处理定时发布
if scheduleTime != nil {
if err := setSchedulePublish(page, *scheduleTime); err != nil {
return errors.Wrap(err, "设置定时发布失败")
}
slog.Info("定时发布设置完成", "schedule_time", scheduleTime.Format("2006-01-02 15:04"))
}
// 设置可见范围
if err := setVisibility(page, visibility); err != nil {
return errors.Wrap(err, "设置可见范围失败")
}
// 等待发布按钮可点击
btn, err := waitForPublishButtonClickable(page)
if err != nil {
return err
}
// 点击发布
if err := btn.Click(proto.InputMouseButtonLeft, 1); err != nil {
return errors.Wrap(err, "点击发布按钮失败")
}
time.Sleep(3 * time.Second)
return nil
}