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:
@@ -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,
|
||||
}
|
||||
|
||||
// 执行发布
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user