feat: 添加小红书标签功能支持 (#65)

* feat: 实现小红书标签输入和自动关联功能

- 在 PublishImageContent 结构体中添加 Tags 字段
- 实现 inputTags 函数处理多个标签输入
- 实现 inputTag 函数自动点击标签联想下拉框
- 通过 #creator-editor-topic-container 选择器定位标签下拉框
- 自动点击第一个 .item 元素完成标签关联
- 添加错误处理和日志记录

* feat: 为 MCP 和 HTTP API 添加标签(tags)支持

- 在 PublishRequest 结构体中添加 Tags 字段
- 更新 MCP handler 处理标签参数
- 更新 MCP 工具定义,添加 tags 参数说明
- HTTP API 自动支持 tags 字段(通过 PublishRequest)
- 保持向后兼容,tags 为可选参数
This commit is contained in:
zy
2025-09-14 15:06:23 +08:00
committed by GitHub
parent cd4e75fa46
commit 47aba20735
4 changed files with 74 additions and 6 deletions

View File

@@ -42,6 +42,7 @@ func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]in
title, _ := args["title"].(string)
content, _ := args["content"].(string)
imagePathsInterface, _ := args["images"].([]interface{})
tagsInterface, _ := args["tags"].([]interface{})
var imagePaths []string
for _, path := range imagePathsInterface {
@@ -50,13 +51,21 @@ func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]in
}
}
logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d", title, len(imagePaths))
var tags []string
for _, tag := range tagsInterface {
if tagStr, ok := tag.(string); ok {
tags = append(tags, tagStr)
}
}
logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d, 标签数量: %d", title, len(imagePaths), len(tags))
// 构建发布请求
req := &PublishRequest{
Title: title,
Content: content,
Images: imagePaths,
Tags: tags,
}
// 执行发布

View File

@@ -24,6 +24,7 @@ type PublishRequest struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
Images []string `json:"images" binding:"required,min=1"`
Tags []string `json:"tags,omitempty"`
}
// LoginStatusResponse 登录状态响应
@@ -89,6 +90,7 @@ func (s *XiaohongshuService) PublishContent(ctx context.Context, req *PublishReq
content := xiaohongshu.PublishImageContent{
Title: req.Title,
Content: req.Content,
Tags: req.Tags,
ImagePaths: imagePaths,
}

View File

@@ -176,7 +176,7 @@ func (s *AppServer) processToolsList(request *JSONRPCRequest) *JSONRPCResponse {
},
"content": map[string]interface{}{
"type": "string",
"description": "正文内容,支持话题标签",
"description": "正文内容",
},
"images": map[string]interface{}{
"type": "array",
@@ -186,6 +186,13 @@ func (s *AppServer) processToolsList(request *JSONRPCRequest) *JSONRPCResponse {
},
"minItems": 1,
},
"tags": map[string]interface{}{
"type": "array",
"description": "话题标签列表(可选),如 [\"美食\", \"旅行\", \"生活\"]",
"items": map[string]interface{}{
"type": "string",
},
},
},
"required": []string{"title", "content", "images"},
},

View File

@@ -15,6 +15,7 @@ import (
type PublishImageContent struct {
Title string
Content string
Tags []string
ImagePaths []string
}
@@ -74,7 +75,7 @@ func (p *PublishAction) Publish(ctx context.Context, content PublishImageContent
return errors.Wrap(err, "小红书上传图片失败")
}
if err := submitPublish(page, content.Title, content.Content); err != nil {
if err := submitPublish(page, content.Title, content.Content, content.Tags); err != nil {
return errors.Wrap(err, "小红书发布失败")
}
@@ -96,7 +97,7 @@ func uploadImages(page *rod.Page, imagesPaths []string) error {
return nil
}
func submitPublish(page *rod.Page, title, content string) error {
func submitPublish(page *rod.Page, title, content string, tags []string) error {
titleElem := page.MustElement("div.d-input input")
titleElem.MustInput(title)
@@ -105,12 +106,13 @@ func submitPublish(page *rod.Page, title, content string) error {
if contentElem, ok := getContentElement(page); ok {
contentElem.MustInput(content)
inputTags(contentElem, tags)
} else {
return errors.New("没有找到内容输入框")
}
time.Sleep(1 * time.Second)
submitButton := page.MustElement("div.submit div.d-button-content")
submitButton.MustClick()
@@ -145,6 +147,54 @@ func getContentElement(page *rod.Page) (*rod.Element, bool) {
return nil, false
}
func inputTags(contentElem *rod.Element, tags []string) {
if len(tags) == 0 {
return
}
contentElem.MustInput("\n\n")
for _, tag := range tags {
tag = strings.TrimLeft(tag, "#")
inputTag(contentElem, tag)
}
}
func inputTag(contentElem *rod.Element, tag string) {
contentElem.MustInput("#")
time.Sleep(200 * time.Millisecond) // 等待标签系统激活
for _, char := range tag {
contentElem.MustInput(string(char))
time.Sleep(50 * time.Millisecond) // 减少延迟时间
}
// 等待标签联想下拉框出现
time.Sleep(500 * time.Millisecond)
page := contentElem.Page()
topicContainer, err := page.Element("#creator-editor-topic-container")
if err == nil && topicContainer != nil {
firstItem, err := topicContainer.Element(".item")
if err == nil && firstItem != nil {
firstItem.MustClick()
slog.Info("成功点击标签联想选项", "tag", tag)
time.Sleep(200 * time.Millisecond) // 等待选择生效
} else {
slog.Warn("未找到标签联想选项,直接输入空格", "tag", tag)
// 如果没有找到联想选项,输入空格结束
contentElem.MustInput(" ")
}
} else {
slog.Warn("未找到标签联想下拉框,直接输入空格", "tag", tag)
// 如果没有找到下拉框,输入空格结束
contentElem.MustInput(" ")
}
time.Sleep(500 * time.Millisecond) // 等待标签处理完成
}
func findTextboxByPlaceholder(page *rod.Page) (*rod.Element, error) {
elements := page.MustElements("p")
if elements == nil {