feat: 添加定时发布功能 (#377)
- publish_content 和 publish_with_video 支持 schedule_at 参数 - 支持 ISO8601 格式时间,范围为 1 小时至 14 天 - 优化页面导航等待策略,提升发布稳定性 Co-authored-by: tanjun <tanjun@tanjundeMac-mini.local> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -132,14 +132,18 @@ func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]in
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d, 标签数量: %d", title, len(imagePaths), len(tags))
|
// 解析定时发布参数
|
||||||
|
scheduleAt, _ := args["schedule_at"].(string)
|
||||||
|
|
||||||
|
logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d, 标签数量: %d, 定时: %s", title, len(imagePaths), len(tags), scheduleAt)
|
||||||
|
|
||||||
// 构建发布请求
|
// 构建发布请求
|
||||||
req := &PublishRequest{
|
req := &PublishRequest{
|
||||||
Title: title,
|
Title: title,
|
||||||
Content: content,
|
Content: content,
|
||||||
Images: imagePaths,
|
Images: imagePaths,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
|
ScheduleAt: scheduleAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行发布
|
// 执行发布
|
||||||
@@ -189,14 +193,18 @@ func (s *AppServer) handlePublishVideo(ctx context.Context, args map[string]inte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("MCP: 发布视频 - 标题: %s, 标签数量: %d", title, len(tags))
|
// 解析定时发布参数
|
||||||
|
scheduleAt, _ := args["schedule_at"].(string)
|
||||||
|
|
||||||
|
logrus.Infof("MCP: 发布视频 - 标题: %s, 标签数量: %d, 定时: %s", title, len(tags), scheduleAt)
|
||||||
|
|
||||||
// 构建发布请求
|
// 构建发布请求
|
||||||
req := &PublishVideoRequest{
|
req := &PublishVideoRequest{
|
||||||
Title: title,
|
Title: title,
|
||||||
Content: content,
|
Content: content,
|
||||||
Video: videoPath,
|
Video: videoPath,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
|
ScheduleAt: scheduleAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行发布
|
// 执行发布
|
||||||
|
|||||||
@@ -17,18 +17,20 @@ func boolPtr(b bool) *bool { return &b }
|
|||||||
|
|
||||||
// PublishContentArgs 发布内容的参数
|
// PublishContentArgs 发布内容的参数
|
||||||
type PublishContentArgs struct {
|
type PublishContentArgs struct {
|
||||||
Title string `json:"title" jsonschema:"内容标题(小红书限制:最多20个中文字或英文单词)"`
|
Title string `json:"title" jsonschema:"内容标题(小红书限制:最多20个中文字或英文单词)"`
|
||||||
Content string `json:"content" jsonschema:"正文内容,不包含以#开头的标签内容,所有话题标签都用tags参数来生成和提供即可"`
|
Content string `json:"content" jsonschema:"正文内容,不包含以#开头的标签内容,所有话题标签都用tags参数来生成和提供即可"`
|
||||||
Images []string `json:"images" jsonschema:"图片路径列表(至少需要1张图片)。支持两种方式:1. HTTP/HTTPS图片链接(自动下载);2. 本地图片绝对路径(推荐,如:/Users/user/image.jpg)"`
|
Images []string `json:"images" jsonschema:"图片路径列表(至少需要1张图片)。支持两种方式:1. HTTP/HTTPS图片链接(自动下载);2. 本地图片绝对路径(推荐,如:/Users/user/image.jpg)"`
|
||||||
Tags []string `json:"tags,omitempty" jsonschema:"话题标签列表(可选参数),如 [美食, 旅行, 生活]"`
|
Tags []string `json:"tags,omitempty" jsonschema:"话题标签列表(可选参数),如 [美食, 旅行, 生活]"`
|
||||||
|
ScheduleAt string `json:"schedule_at,omitempty" jsonschema:"定时发布时间(可选),ISO8601格式如 2024-01-20T10:30:00+08:00,支持1小时至14天内。不填则立即发布"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublishVideoArgs 发布视频的参数(仅支持本地单个视频文件)
|
// PublishVideoArgs 发布视频的参数(仅支持本地单个视频文件)
|
||||||
type PublishVideoArgs struct {
|
type PublishVideoArgs struct {
|
||||||
Title string `json:"title" jsonschema:"内容标题(小红书限制:最多20个中文字或英文单词)"`
|
Title string `json:"title" jsonschema:"内容标题(小红书限制:最多20个中文字或英文单词)"`
|
||||||
Content string `json:"content" jsonschema:"正文内容,不包含以#开头的标签内容,所有话题标签都用tags参数来生成和提供即可"`
|
Content string `json:"content" jsonschema:"正文内容,不包含以#开头的标签内容,所有话题标签都用tags参数来生成和提供即可"`
|
||||||
Video string `json:"video" jsonschema:"本地视频绝对路径(仅支持单个视频文件,如:/Users/user/video.mp4)"`
|
Video string `json:"video" jsonschema:"本地视频绝对路径(仅支持单个视频文件,如:/Users/user/video.mp4)"`
|
||||||
Tags []string `json:"tags,omitempty" jsonschema:"话题标签列表(可选参数),如 [美食, 旅行, 生活]"`
|
Tags []string `json:"tags,omitempty" jsonschema:"话题标签列表(可选参数),如 [美食, 旅行, 生活]"`
|
||||||
|
ScheduleAt string `json:"schedule_at,omitempty" jsonschema:"定时发布时间(可选),ISO8601格式如 2024-01-20T10:30:00+08:00,支持1小时至14天内。不填则立即发布"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchFeedsArgs 搜索内容的参数
|
// SearchFeedsArgs 搜索内容的参数
|
||||||
@@ -207,10 +209,11 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
|
|||||||
withPanicRecovery("publish_content", func(ctx context.Context, req *mcp.CallToolRequest, args PublishContentArgs) (*mcp.CallToolResult, any, error) {
|
withPanicRecovery("publish_content", func(ctx context.Context, req *mcp.CallToolRequest, args PublishContentArgs) (*mcp.CallToolResult, any, error) {
|
||||||
// 转换参数格式到现有的 handler
|
// 转换参数格式到现有的 handler
|
||||||
argsMap := map[string]interface{}{
|
argsMap := map[string]interface{}{
|
||||||
"title": args.Title,
|
"title": args.Title,
|
||||||
"content": args.Content,
|
"content": args.Content,
|
||||||
"images": convertStringsToInterfaces(args.Images),
|
"images": convertStringsToInterfaces(args.Images),
|
||||||
"tags": convertStringsToInterfaces(args.Tags),
|
"tags": convertStringsToInterfaces(args.Tags),
|
||||||
|
"schedule_at": args.ScheduleAt,
|
||||||
}
|
}
|
||||||
result := appServer.handlePublishContent(ctx, argsMap)
|
result := appServer.handlePublishContent(ctx, argsMap)
|
||||||
return convertToMCPResult(result), nil, nil
|
return convertToMCPResult(result), nil, nil
|
||||||
@@ -377,10 +380,11 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
|
|||||||
},
|
},
|
||||||
withPanicRecovery("publish_with_video", func(ctx context.Context, req *mcp.CallToolRequest, args PublishVideoArgs) (*mcp.CallToolResult, any, error) {
|
withPanicRecovery("publish_with_video", func(ctx context.Context, req *mcp.CallToolRequest, args PublishVideoArgs) (*mcp.CallToolResult, any, error) {
|
||||||
argsMap := map[string]interface{}{
|
argsMap := map[string]interface{}{
|
||||||
"title": args.Title,
|
"title": args.Title,
|
||||||
"content": args.Content,
|
"content": args.Content,
|
||||||
"video": args.Video,
|
"video": args.Video,
|
||||||
"tags": convertStringsToInterfaces(args.Tags),
|
"tags": convertStringsToInterfaces(args.Tags),
|
||||||
|
"schedule_at": args.ScheduleAt,
|
||||||
}
|
}
|
||||||
result := appServer.handlePublishVideo(ctx, argsMap)
|
result := appServer.handlePublishVideo(ctx, argsMap)
|
||||||
return convertToMCPResult(result), nil, nil
|
return convertToMCPResult(result), nil, nil
|
||||||
|
|||||||
88
service.go
88
service.go
@@ -28,10 +28,11 @@ func NewXiaohongshuService() *XiaohongshuService {
|
|||||||
|
|
||||||
// PublishRequest 发布请求
|
// PublishRequest 发布请求
|
||||||
type PublishRequest struct {
|
type PublishRequest struct {
|
||||||
Title string `json:"title" binding:"required"`
|
Title string `json:"title" binding:"required"`
|
||||||
Content string `json:"content" binding:"required"`
|
Content string `json:"content" binding:"required"`
|
||||||
Images []string `json:"images" binding:"required,min=1"`
|
Images []string `json:"images" binding:"required,min=1"`
|
||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
ScheduleAt string `json:"schedule_at,omitempty"` // 定时发布时间,ISO8601格式,为空则立即发布
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginStatusResponse 登录状态响应
|
// LoginStatusResponse 登录状态响应
|
||||||
@@ -58,10 +59,11 @@ type PublishResponse struct {
|
|||||||
|
|
||||||
// PublishVideoRequest 发布视频请求(仅支持本地单个视频文件)
|
// PublishVideoRequest 发布视频请求(仅支持本地单个视频文件)
|
||||||
type PublishVideoRequest struct {
|
type PublishVideoRequest struct {
|
||||||
Title string `json:"title" binding:"required"`
|
Title string `json:"title" binding:"required"`
|
||||||
Content string `json:"content" binding:"required"`
|
Content string `json:"content" binding:"required"`
|
||||||
Video string `json:"video" binding:"required"`
|
Video string `json:"video" binding:"required"`
|
||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
ScheduleAt string `json:"schedule_at,omitempty"` // 定时发布时间,ISO8601格式,为空则立即发布
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublishVideoResponse 发布视频响应
|
// PublishVideoResponse 发布视频响应
|
||||||
@@ -179,12 +181,39 @@ func (s *XiaohongshuService) PublishContent(ctx context.Context, req *PublishReq
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析定时发布时间
|
||||||
|
var scheduleTime *time.Time
|
||||||
|
if req.ScheduleAt != "" {
|
||||||
|
t, err := time.Parse(time.RFC3339, req.ScheduleAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("定时发布时间格式错误,请使用 ISO8601 格式: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验定时发布时间范围:1小时至14天
|
||||||
|
now := time.Now()
|
||||||
|
minTime := now.Add(1 * time.Hour)
|
||||||
|
maxTime := now.Add(14 * 24 * time.Hour)
|
||||||
|
|
||||||
|
if t.Before(minTime) {
|
||||||
|
return nil, fmt.Errorf("定时发布时间必须至少在1小时后,当前设置: %s,最早可选: %s",
|
||||||
|
t.Format("2006-01-02 15:04"), minTime.Format("2006-01-02 15:04"))
|
||||||
|
}
|
||||||
|
if t.After(maxTime) {
|
||||||
|
return nil, fmt.Errorf("定时发布时间不能超过14天,当前设置: %s,最晚可选: %s",
|
||||||
|
t.Format("2006-01-02 15:04"), maxTime.Format("2006-01-02 15:04"))
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleTime = &t
|
||||||
|
logrus.Infof("设置定时发布时间: %s", t.Format("2006-01-02 15:04"))
|
||||||
|
}
|
||||||
|
|
||||||
// 构建发布内容
|
// 构建发布内容
|
||||||
content := xiaohongshu.PublishImageContent{
|
content := xiaohongshu.PublishImageContent{
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
Content: req.Content,
|
Content: req.Content,
|
||||||
Tags: req.Tags,
|
Tags: req.Tags,
|
||||||
ImagePaths: imagePaths,
|
ImagePaths: imagePaths,
|
||||||
|
ScheduleTime: scheduleTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行发布
|
// 执行发布
|
||||||
@@ -241,12 +270,39 @@ func (s *XiaohongshuService) PublishVideo(ctx context.Context, req *PublishVideo
|
|||||||
return nil, fmt.Errorf("视频文件不存在或不可访问: %v", err)
|
return nil, fmt.Errorf("视频文件不存在或不可访问: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析定时发布时间
|
||||||
|
var scheduleTime *time.Time
|
||||||
|
if req.ScheduleAt != "" {
|
||||||
|
t, err := time.Parse(time.RFC3339, req.ScheduleAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("定时发布时间格式错误,请使用 ISO8601 格式: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验定时发布时间范围:1小时至14天
|
||||||
|
now := time.Now()
|
||||||
|
minTime := now.Add(1 * time.Hour)
|
||||||
|
maxTime := now.Add(14 * 24 * time.Hour)
|
||||||
|
|
||||||
|
if t.Before(minTime) {
|
||||||
|
return nil, fmt.Errorf("定时发布时间必须至少在1小时后,当前设置: %s,最早可选: %s",
|
||||||
|
t.Format("2006-01-02 15:04"), minTime.Format("2006-01-02 15:04"))
|
||||||
|
}
|
||||||
|
if t.After(maxTime) {
|
||||||
|
return nil, fmt.Errorf("定时发布时间不能超过14天,当前设置: %s,最晚可选: %s",
|
||||||
|
t.Format("2006-01-02 15:04"), maxTime.Format("2006-01-02 15:04"))
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleTime = &t
|
||||||
|
logrus.Infof("设置定时发布时间: %s", t.Format("2006-01-02 15:04"))
|
||||||
|
}
|
||||||
|
|
||||||
// 构建发布内容
|
// 构建发布内容
|
||||||
content := xiaohongshu.PublishVideoContent{
|
content := xiaohongshu.PublishVideoContent{
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
Content: req.Content,
|
Content: req.Content,
|
||||||
Tags: req.Tags,
|
Tags: req.Tags,
|
||||||
VideoPath: req.Video,
|
VideoPath: req.Video,
|
||||||
|
ScheduleTime: scheduleTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行发布
|
// 执行发布
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ import (
|
|||||||
|
|
||||||
// PublishImageContent 发布图文内容
|
// PublishImageContent 发布图文内容
|
||||||
type PublishImageContent struct {
|
type PublishImageContent struct {
|
||||||
Title string
|
Title string
|
||||||
Content string
|
Content string
|
||||||
Tags []string
|
Tags []string
|
||||||
ImagePaths []string
|
ImagePaths []string
|
||||||
|
ScheduleTime *time.Time // 定时发布时间,nil 表示立即发布
|
||||||
}
|
}
|
||||||
|
|
||||||
type PublishAction struct {
|
type PublishAction struct {
|
||||||
@@ -35,7 +36,21 @@ func NewPublishImageAction(page *rod.Page) (*PublishAction, error) {
|
|||||||
|
|
||||||
pp := page.Timeout(300 * time.Second)
|
pp := page.Timeout(300 * time.Second)
|
||||||
|
|
||||||
pp.MustNavigate(urlOfPublic).MustWaitIdle().MustWaitDOMStable()
|
// 使用更稳健的导航和等待策略
|
||||||
|
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)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
if err := mustClickPublishTab(pp, "上传图文"); err != nil {
|
if err := mustClickPublishTab(pp, "上传图文"); err != nil {
|
||||||
@@ -67,9 +82,9 @@ func (p *PublishAction) Publish(ctx context.Context, content PublishImageContent
|
|||||||
tags = tags[:10]
|
tags = tags[:10]
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("发布内容: title=%s, images=%v, tags=%v", content.Title, len(content.ImagePaths), tags)
|
logrus.Infof("发布内容: title=%s, images=%v, tags=%v, schedule=%v", content.Title, len(content.ImagePaths), tags, content.ScheduleTime)
|
||||||
|
|
||||||
if err := submitPublish(page, content.Title, content.Content, tags); err != nil {
|
if err := submitPublish(page, content.Title, content.Content, tags, content.ScheduleTime); err != nil {
|
||||||
return errors.Wrap(err, "小红书发布失败")
|
return errors.Wrap(err, "小红书发布失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +254,7 @@ func waitForUploadComplete(page *rod.Page, expectedCount int) error {
|
|||||||
return errors.New("上传超时,请检查网络连接和图片大小")
|
return errors.New("上传超时,请检查网络连接和图片大小")
|
||||||
}
|
}
|
||||||
|
|
||||||
func submitPublish(page *rod.Page, title, content string, tags []string) error {
|
func submitPublish(page *rod.Page, title, content string, tags []string, scheduleTime *time.Time) error {
|
||||||
|
|
||||||
titleElem := page.MustElement("div.d-input input")
|
titleElem := page.MustElement("div.d-input input")
|
||||||
titleElem.MustInput(title)
|
titleElem.MustInput(title)
|
||||||
@@ -270,6 +285,14 @@ func submitPublish(page *rod.Page, title, content string, tags []string) error {
|
|||||||
}
|
}
|
||||||
slog.Info("检查正文长度:通过")
|
slog.Info("检查正文长度:通过")
|
||||||
|
|
||||||
|
// 处理定时发布
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
|
||||||
submitButton := page.MustElement("div.submit div.d-button-content")
|
submitButton := page.MustElement("div.submit div.d-button-content")
|
||||||
submitButton.MustClick()
|
submitButton.MustClick()
|
||||||
|
|
||||||
@@ -499,3 +522,132 @@ func isElementVisible(elem *rod.Element) bool {
|
|||||||
|
|
||||||
return visible
|
return visible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setSchedulePublish 设置定时发布时间
|
||||||
|
func setSchedulePublish(page *rod.Page, t time.Time) error {
|
||||||
|
// 1. 点击"定时发布" radio button
|
||||||
|
if err := clickScheduleRadio(page); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
// 2. 点击时间选择器打开面板
|
||||||
|
if err := clickDateTimePicker(page); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
// 3. 设置日期和时间
|
||||||
|
if err := setDateTime(page, t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
|
||||||
|
// 4. 点击确定按钮
|
||||||
|
if err := clickConfirmButton(page); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clickScheduleRadio 点击定时发布 radio
|
||||||
|
func clickScheduleRadio(page *rod.Page) error {
|
||||||
|
labels, err := page.Elements("span.el-radio__label")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "查找 radio label 失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range labels {
|
||||||
|
text, err := label.Text()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(text) == "定时发布" {
|
||||||
|
if err := label.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||||
|
return errors.Wrap(err, "点击定时发布按钮失败")
|
||||||
|
}
|
||||||
|
slog.Info("已点击定时发布按钮")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("未找到定时发布按钮")
|
||||||
|
}
|
||||||
|
|
||||||
|
// clickDateTimePicker 点击时间选择器
|
||||||
|
func clickDateTimePicker(page *rod.Page) error {
|
||||||
|
// 查找日期时间选择器输入框
|
||||||
|
picker, err := page.Element("input.el-input__inner[placeholder='选择日期和时间']")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "查找时间选择器失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := picker.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||||
|
return errors.Wrap(err, "点击时间选择器失败")
|
||||||
|
}
|
||||||
|
slog.Info("已点击时间选择器")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDateTime 设置日期和时间
|
||||||
|
func setDateTime(page *rod.Page, t time.Time) error {
|
||||||
|
dateStr := t.Format("2006-01-02")
|
||||||
|
timeStr := t.Format("15:04")
|
||||||
|
|
||||||
|
// 设置日期
|
||||||
|
dateInput, err := page.Element("input.el-input__inner[placeholder='选择日期']")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "查找日期输入框失败")
|
||||||
|
}
|
||||||
|
if err := dateInput.SelectAllText(); err != nil {
|
||||||
|
return errors.Wrap(err, "选择日期文本失败")
|
||||||
|
}
|
||||||
|
if err := dateInput.Input(dateStr); err != nil {
|
||||||
|
return errors.Wrap(err, "输入日期失败")
|
||||||
|
}
|
||||||
|
slog.Info("已设置日期", "date", dateStr)
|
||||||
|
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
|
||||||
|
// 设置时间
|
||||||
|
timeInput, err := page.Element("input.el-input__inner[placeholder='选择时间']")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "查找时间输入框失败")
|
||||||
|
}
|
||||||
|
if err := timeInput.SelectAllText(); err != nil {
|
||||||
|
return errors.Wrap(err, "选择时间文本失败")
|
||||||
|
}
|
||||||
|
if err := timeInput.Input(timeStr); err != nil {
|
||||||
|
return errors.Wrap(err, "输入时间失败")
|
||||||
|
}
|
||||||
|
slog.Info("已设置时间", "time", timeStr)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clickConfirmButton 点击确定按钮
|
||||||
|
func clickConfirmButton(page *rod.Page) error {
|
||||||
|
// 查找日期选择器弹窗中的确定按钮
|
||||||
|
buttons, err := page.Elements("button.el-picker-panel__link-btn")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "查找确定按钮失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, btn := range buttons {
|
||||||
|
text, err := btn.Text()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(text) == "确定" {
|
||||||
|
if err := btn.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||||
|
return errors.Wrap(err, "点击确定按钮失败")
|
||||||
|
}
|
||||||
|
slog.Info("已点击确定按钮")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("未找到确定按钮")
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ import (
|
|||||||
|
|
||||||
// PublishVideoContent 发布视频内容
|
// PublishVideoContent 发布视频内容
|
||||||
type PublishVideoContent struct {
|
type PublishVideoContent struct {
|
||||||
Title string
|
Title string
|
||||||
Content string
|
Content string
|
||||||
Tags []string
|
Tags []string
|
||||||
VideoPath string
|
VideoPath string
|
||||||
|
ScheduleTime *time.Time // 定时发布时间,nil 表示立即发布
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPublishVideoAction 进入发布页并切换到“上传视频”
|
// NewPublishVideoAction 进入发布页并切换到“上传视频”
|
||||||
@@ -48,7 +49,7 @@ func (p *PublishAction) PublishVideo(ctx context.Context, content PublishVideoCo
|
|||||||
return errors.Wrap(err, "小红书上传视频失败")
|
return errors.Wrap(err, "小红书上传视频失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := submitPublishVideo(page, content.Title, content.Content, content.Tags); err != nil {
|
if err := submitPublishVideo(page, content.Title, content.Content, content.Tags, content.ScheduleTime); err != nil {
|
||||||
return errors.Wrap(err, "小红书发布失败")
|
return errors.Wrap(err, "小红书发布失败")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -116,7 +117,7 @@ func waitForPublishButtonClickable(page *rod.Page) (*rod.Element, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// submitPublishVideo 填写标题、正文、标签并点击发布(等待按钮可点击后再提交)
|
// submitPublishVideo 填写标题、正文、标签并点击发布(等待按钮可点击后再提交)
|
||||||
func submitPublishVideo(page *rod.Page, title, content string, tags []string) error {
|
func submitPublishVideo(page *rod.Page, title, content string, tags []string, scheduleTime *time.Time) error {
|
||||||
// 标题
|
// 标题
|
||||||
titleElem := page.MustElement("div.d-input input")
|
titleElem := page.MustElement("div.d-input input")
|
||||||
titleElem.MustInput(title)
|
titleElem.MustInput(title)
|
||||||
@@ -132,6 +133,14 @@ func submitPublishVideo(page *rod.Page, title, content string, tags []string) er
|
|||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
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"))
|
||||||
|
}
|
||||||
|
|
||||||
// 等待发布按钮可点击
|
// 等待发布按钮可点击
|
||||||
btn, err := waitForPublishButtonClickable(page)
|
btn, err := waitForPublishButtonClickable(page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user