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)
|
title, _ := args["title"].(string)
|
||||||
content, _ := args["content"].(string)
|
content, _ := args["content"].(string)
|
||||||
imagePathsInterface, _ := args["images"].([]interface{})
|
imagePathsInterface, _ := args["images"].([]interface{})
|
||||||
|
tagsInterface, _ := args["tags"].([]interface{})
|
||||||
|
|
||||||
var imagePaths []string
|
var imagePaths []string
|
||||||
for _, path := range imagePathsInterface {
|
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{
|
req := &PublishRequest{
|
||||||
Title: title,
|
Title: title,
|
||||||
Content: content,
|
Content: content,
|
||||||
Images: imagePaths,
|
Images: imagePaths,
|
||||||
|
Tags: tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行发布
|
// 执行发布
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginStatusResponse 登录状态响应
|
// LoginStatusResponse 登录状态响应
|
||||||
@@ -89,6 +90,7 @@ func (s *XiaohongshuService) PublishContent(ctx context.Context, req *PublishReq
|
|||||||
content := xiaohongshu.PublishImageContent{
|
content := xiaohongshu.PublishImageContent{
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
Content: req.Content,
|
Content: req.Content,
|
||||||
|
Tags: req.Tags,
|
||||||
ImagePaths: imagePaths,
|
ImagePaths: imagePaths,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ func (s *AppServer) processToolsList(request *JSONRPCRequest) *JSONRPCResponse {
|
|||||||
},
|
},
|
||||||
"content": map[string]interface{}{
|
"content": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "正文内容,支持话题标签",
|
"description": "正文内容",
|
||||||
},
|
},
|
||||||
"images": map[string]interface{}{
|
"images": map[string]interface{}{
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -186,6 +186,13 @@ func (s *AppServer) processToolsList(request *JSONRPCRequest) *JSONRPCResponse {
|
|||||||
},
|
},
|
||||||
"minItems": 1,
|
"minItems": 1,
|
||||||
},
|
},
|
||||||
|
"tags": map[string]interface{}{
|
||||||
|
"type": "array",
|
||||||
|
"description": "话题标签列表(可选),如 [\"美食\", \"旅行\", \"生活\"]",
|
||||||
|
"items": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": []string{"title", "content", "images"},
|
"required": []string{"title", "content", "images"},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
type PublishImageContent struct {
|
type PublishImageContent struct {
|
||||||
Title string
|
Title string
|
||||||
Content string
|
Content string
|
||||||
|
Tags []string
|
||||||
ImagePaths []string
|
ImagePaths []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ func (p *PublishAction) Publish(ctx context.Context, content PublishImageContent
|
|||||||
return errors.Wrap(err, "小红书上传图片失败")
|
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, "小红书发布失败")
|
return errors.Wrap(err, "小红书发布失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ func uploadImages(page *rod.Page, imagesPaths []string) error {
|
|||||||
return nil
|
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 := page.MustElement("div.d-input input")
|
||||||
titleElem.MustInput(title)
|
titleElem.MustInput(title)
|
||||||
@@ -105,12 +106,13 @@ func submitPublish(page *rod.Page, title, content string) error {
|
|||||||
|
|
||||||
if contentElem, ok := getContentElement(page); ok {
|
if contentElem, ok := getContentElement(page); ok {
|
||||||
contentElem.MustInput(content)
|
contentElem.MustInput(content)
|
||||||
|
|
||||||
|
inputTags(contentElem, tags)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return errors.New("没有找到内容输入框")
|
return errors.New("没有找到内容输入框")
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
submitButton := page.MustElement("div.submit div.d-button-content")
|
submitButton := page.MustElement("div.submit div.d-button-content")
|
||||||
submitButton.MustClick()
|
submitButton.MustClick()
|
||||||
|
|
||||||
@@ -145,6 +147,54 @@ func getContentElement(page *rod.Page) (*rod.Element, bool) {
|
|||||||
return nil, false
|
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) {
|
func findTextboxByPlaceholder(page *rod.Page) (*rod.Element, error) {
|
||||||
elements := page.MustElements("p")
|
elements := page.MustElements("p")
|
||||||
if elements == nil {
|
if elements == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user