feat: 为视频发布功能新增HTTP API接口和完善文档 (#179)

* 重构 publish tab 选择逻辑,把公共代码提取到同一个函数中

* feat: 为视频发布功能新增HTTP API接口和完善文档

- 新增 /api/v1/publish_video HTTP接口
- 添加 publishVideoHandler 处理函数
- 更新 API.md 增加视频发布接口文档
- 更新 README.md 和 README_EN.md 增加视频发布功能说明
- 在MCP工具列表中补充 publish_with_video 工具说明
This commit is contained in:
zy
2025-09-29 01:08:11 +08:00
committed by GitHub
parent 8c3665a3de
commit 0955723b19
7 changed files with 312 additions and 185 deletions

View File

@@ -36,7 +36,7 @@ https://github.com/user-attachments/assets/bd9a9a4a-58cb-4421-b8f3-015f703ce1f9
<details>
<summary><b>2. 发布图文内容</b></summary>
支持发布图文内容到小红书,包括标题、内容描述和图片。后续支持更多的发布功能。
支持发布图文内容到小红书,包括标题、内容描述和图片。
**图片支持方式:**
@@ -67,7 +67,35 @@ https://github.com/user-attachments/assets/8aee0814-eb96-40af-b871-e66e6bbb6b06
</details>
<details>
<summary><b>3. 搜索内容</b></summary>
<summary><b>3. 发布视频内容</b></summary>
支持发布视频内容到小红书,包括标题、内容描述和本地视频文件。
**视频支持方式:**
仅支持本地视频文件绝对路径:
```
"/Users/username/Videos/video.mp4"
```
**功能特点:**
- ✅ 支持本地视频文件上传
- ✅ 自动处理视频格式转换
- ✅ 支持标题、内容描述和标签
- ✅ 等待视频处理完成后自动发布
**注意事项:**
- 仅支持本地视频文件,不支持 HTTP 链接
- 视频处理时间较长,请耐心等待
- 建议视频文件大小不超过 1GB
</details>
<details>
<summary><b>4. 搜索内容</b></summary>
根据关键词搜索小红书内容。
@@ -78,7 +106,7 @@ https://github.com/user-attachments/assets/03c5077d-6160-4b18-b629-2e40933a1fd3
</details>
<details>
<summary><b>4. 获取推荐列表</b></summary>
<summary><b>5. 获取推荐列表</b></summary>
获取小红书首页推荐内容列表。
@@ -89,7 +117,7 @@ https://github.com/user-attachments/assets/110fc15d-46f2-4cca-bdad-9de5b5b8cc28
</details>
<details>
<summary><b>5. 获取帖子详情(包括互动数据和评论)</b></summary>
<summary><b>6. 获取帖子详情(包括互动数据和评论)</b></summary>
获取小红书帖子的完整详情,包括:
@@ -111,7 +139,7 @@ https://github.com/user-attachments/assets/76a26130-a216-4371-a6b3-937b8fda092a
</details>
<details>
<summary><b>6. 发表评论到帖子</b></summary>
<summary><b>7. 发表评论到帖子</b></summary>
支持自动发表评论到小红书帖子。
@@ -134,7 +162,7 @@ https://github.com/user-attachments/assets/cc385b6c-422c-489b-a5fc-63e92c695b80
</details>
<details>
<summary><b>7. 获取用户个人主页</b></summary>
<summary><b>8. 获取用户个人主页</b></summary>
获取小红书用户的个人主页信息,包括用户基本信息和笔记内容。
@@ -637,6 +665,8 @@ Cline 是一个强大的 AI 编程助手,支持 MCP 协议集成。
- `check_login_status` - 检查小红书登录状态(无参数)
- `publish_content` - 发布图文内容到小红书必需title, content, images
- `images`: 支持 HTTP 链接或本地绝对路径,推荐使用本地路径
- `publish_with_video` - 发布视频内容到小红书必需title, content, video
- `video`: 仅支持本地视频文件绝对路径
- `list_feeds` - 获取小红书首页推荐列表(无参数)
- `search_feeds` - 搜索小红书内容需要keyword
- `get_feed_detail` - 获取帖子详情需要feed_id, xsec_token
@@ -668,6 +698,16 @@ Cline 是一个强大的 AI 编程助手,支持 MCP 协议集成。
使用 xiaohongshu-mcp 进行发布。
```
**示例 3发布视频内容**
```
帮我写一篇关于美食制作的视频发布到小红书上,
使用这个本地视频文件:
- /Users/username/Videos/cooking_tutorial.mp4
使用 xiaohongshu-mcp 的视频发布功能。
```
![claude-cli 进行发布](./assets/claude_push.gif)
**发布结果:**

View File

@@ -36,7 +36,7 @@ https://github.com/user-attachments/assets/bd9a9a4a-58cb-4421-b8f3-015f703ce1f9
<details>
<summary><b>2. Publish Image and Text Content</b></summary>
Supports publishing image and text content to RedNote, including title, content description, and images. More publishing features will be supported later.
Supports publishing image and text content to RedNote, including title, content description, and images.
**Image Support Methods:**
@@ -67,7 +67,35 @@ https://github.com/user-attachments/assets/8aee0814-eb96-40af-b871-e66e6bbb6b06
</details>
<details>
<summary><b>3. Search Content</b></summary>
<summary><b>3. Publish Video Content</b></summary>
Supports publishing video content to RedNote, including title, content description, and local video files.
**Video Support Methods:**
Only supports local video file absolute paths:
```
"/Users/username/Videos/video.mp4"
```
**Features:**
- ✅ Supports local video file upload
- ✅ Automatic video format processing
- ✅ Supports title, content description, and tags
- ✅ Automatically publishes after video processing is complete
**Important Notes:**
- Only supports local video files, not HTTP links
- Video processing takes longer, please be patient
- Recommended video file size should not exceed 1GB
</details>
<details>
<summary><b>4. Search Content</b></summary>
Search RedNote content by keywords.
@@ -78,7 +106,7 @@ https://github.com/user-attachments/assets/03c5077d-6160-4b18-b629-2e40933a1fd3
</details>
<details>
<summary><b>4. Get Recommendation List</b></summary>
<summary><b>5. Get Recommendation List</b></summary>
Get RedNote homepage recommendation content list.
@@ -89,7 +117,7 @@ https://github.com/user-attachments/assets/110fc15d-46f2-4cca-bdad-9de5b5b8cc28
</details>
<details>
<summary><b>5. Get Post Details (Including Interaction Data and Comments)</b></summary>
<summary><b>6. Get Post Details (Including Interaction Data and Comments)</b></summary>
Get complete details of RedNote posts, including:
@@ -111,7 +139,7 @@ https://github.com/user-attachments/assets/76a26130-a216-4371-a6b3-937b8fda092a
</details>
<details>
<summary><b>6. Post Comments to Posts</b></summary>
<summary><b>7. Post Comments to Posts</b></summary>
Supports automatically posting comments to RedNote posts.
@@ -134,7 +162,7 @@ https://github.com/user-attachments/assets/cc385b6c-422c-489b-a5fc-63e92c695b80
</details>
<details>
<summary><b>7. Get User Profile</b></summary>
<summary><b>8. Get User Profile</b></summary>
Get RedNote user's personal profile information, including basic user information and note content.
@@ -633,6 +661,8 @@ After successful connection, you can use the following MCP tools:
- `check_login_status` - Check RedNote login status (no parameters)
- `publish_content` - Publish image-text content to RedNote (required: title, content, images)
- `images`: Supports HTTP links or local absolute paths, local paths recommended
- `publish_with_video` - Publish video content to RedNote (required: title, content, video)
- `video`: Only supports local video file absolute paths
- `list_feeds` - Get RedNote homepage recommendation list (no parameters)
- `search_feeds` - Search RedNote content (required: keyword)
- `get_feed_detail` - Get post details (required: feed_id, xsec_token)
@@ -664,6 +694,16 @@ using these local images:
Use xiaohongshu-mcp for publishing.
```
**Example 3: Publishing Video Content**
```
Help me write a video post about cooking tutorials to publish on RedNote,
using this local video file:
- /Users/username/Videos/cooking_tutorial.mp4
Use xiaohongshu-mcp's video publishing feature.
```
![claude-cli publishing](./assets/claude_push.gif)
**Publishing Result:**

View File

@@ -111,7 +111,9 @@ GET /api/v1/login/qrcode
### 3. 内容发布
发布笔记内容到小红书。
#### 3.1 发布图文内容
发布图文笔记内容到小红书。
**请求**
```
@@ -153,6 +155,52 @@ Content-Type: application/json
}
```
#### 3.2 发布视频内容
发布视频内容到小红书(仅支持本地视频文件)。
**请求**
```
POST /api/v1/publish_video
Content-Type: application/json
```
**请求体**
```json
{
"title": "视频标题",
"content": "视频内容描述",
"video": "/Users/username/Videos/video.mp4",
"tags": ["标签1", "标签2"]
}
```
**请求参数说明:**
- `title` (string, required): 视频标题
- `content` (string, required): 视频内容描述
- `video` (string, required): 本地视频文件绝对路径
- `tags` (array, optional): 标签数组
**响应**
```json
{
"success": true,
"data": {
"title": "视频标题",
"content": "视频内容描述",
"video": "/Users/username/Videos/video.mp4",
"status": "发布完成",
"post_id": "64f1a2b3c4d5e6f7a8b9c0d1"
},
"message": "视频发布成功"
}
```
**注意事项:**
- 仅支持本地视频文件路径,不支持 HTTP 链接
- 视频处理时间较长,请耐心等待
- 建议视频文件大小不超过 1GB
---
### 4. Feed 管理

View File

@@ -81,6 +81,26 @@ func (s *AppServer) publishHandler(c *gin.Context) {
respondSuccess(c, result, "发布成功")
}
// publishVideoHandler 发布视频内容
func (s *AppServer) publishVideoHandler(c *gin.Context) {
var req PublishVideoRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "INVALID_REQUEST",
"请求参数错误", err.Error())
return
}
// 执行视频发布
result, err := s.xiaohongshuService.PublishVideo(c.Request.Context(), &req)
if err != nil {
respondError(c, http.StatusInternalServerError, "PUBLISH_VIDEO_FAILED",
"视频发布失败", err.Error())
return
}
respondSuccess(c, result, "视频发布成功")
}
// listFeedsHandler 获取Feeds列表
func (s *AppServer) listFeedsHandler(c *gin.Context) {
// 获取 Feeds 列表

View File

@@ -41,6 +41,7 @@ func setupRoutes(appServer *AppServer) *gin.Engine {
api.GET("/login/status", appServer.checkLoginStatusHandler)
api.GET("/login/qrcode", appServer.getLoginQrcodeHandler)
api.POST("/publish", appServer.publishHandler)
api.POST("/publish_video", appServer.publishVideoHandler)
api.GET("/feeds/list", appServer.listFeedsHandler)
api.GET("/feeds/search", appServer.searchFeedsHandler)
api.POST("/feeds/detail", appServer.getFeedDetailHandler)

View File

@@ -37,45 +37,7 @@ func NewPublishImageAction(page *rod.Page) (*PublishAction, error) {
pp.MustNavigate(urlOfPublic).MustWaitIdle().MustWaitDOMStable()
time.Sleep(1 * time.Second)
logrus.Info("navigate to publish page success")
removePopCover(page) // 移除弹窗封面
pp.MustElement(`div.upload-content`).MustWaitVisible()
slog.Info("wait for upload-content visible success")
// 等待一段时间确保页面完全加载
time.Sleep(1 * time.Second)
createElems := pp.MustElements("div.creator-tab")
// 过滤掉隐藏的元素
var visibleElems []*rod.Element
for _, elem := range createElems {
if isElementVisible(elem) {
visibleElems = append(visibleElems, elem)
}
}
if len(visibleElems) == 0 {
return nil, errors.New("没有找到上传图文元素")
}
for _, elem := range visibleElems {
text, err := elem.Text()
if err != nil {
slog.Error("获取元素文本失败", "error", err)
continue
}
if text == "上传图文" {
if err := elem.Click(proto.InputMouseButtonLeft, 1); err != nil {
slog.Error("点击元素失败", "error", err)
continue
}
break
}
}
mustClickPublishTab(page, "上传图文")
time.Sleep(1 * time.Second)
@@ -115,6 +77,54 @@ func removePopCover(page *rod.Page) {
}
func mustClickPublishTab(page *rod.Page, tabname string) error {
removePopCover(page) // 移除弹窗封面
page.MustElement(`div.upload-content`).MustWaitVisible()
time.Sleep(1 * time.Second)
createElems := page.MustElements("div.creator-tab")
// 过滤掉隐藏的元素
var visibleElems []*rod.Element
for _, elem := range createElems {
if isElementVisible(elem) {
visibleElems = append(visibleElems, elem)
}
}
if len(visibleElems) == 0 {
return errors.Errorf("没有找到发布 TAB - %s", tabname)
}
var clicked bool
for _, elem := range visibleElems {
text, err := elem.Text()
if err != nil {
logrus.Errorf("获取元素文本失败: %v", err)
continue
}
if text == tabname {
if err := elem.Click(proto.InputMouseButtonLeft, 1); err != nil {
logrus.Errorf("点击元素失败: %v", err)
continue
}
clicked = true
break
}
}
if !clicked {
return errors.Errorf("没有找到发布 TAB - %s", tabname)
}
return nil
}
func uploadImages(page *rod.Page, imagesPaths []string) error {
pp := page.Timeout(30 * time.Second)

View File

@@ -24,43 +24,11 @@ type PublishVideoContent struct {
func NewPublishVideoAction(page *rod.Page) (*PublishAction, error) {
pp := page.Timeout(300 * time.Second)
pp.MustNavigate(urlOfPublic)
removePopCover(page) // 移除弹窗封面
pp.MustElement(`div.upload-content`).MustWaitVisible()
slog.Info("wait for upload-content visible success (video)")
pp.MustNavigate(urlOfPublic).MustWaitIdle().MustWaitDOMStable()
time.Sleep(1 * time.Second)
createElems := pp.MustElements("div.creator-tab")
// 过滤掉隐藏的元素
var visibleElems []*rod.Element
for _, elem := range createElems {
if isElementVisible(elem) {
visibleElems = append(visibleElems, elem)
}
}
if len(visibleElems) == 0 {
return nil, errors.New("没有找到上传视频元素")
}
// 点击“上传视频”
for _, elem := range visibleElems {
text, err := elem.Text()
if err != nil {
slog.Error("获取元素文本失败", "error", err)
continue
}
if text == "上传视频" {
if err := elem.Click(proto.InputMouseButtonLeft, 1); err != nil {
slog.Error("点击元素失败", "error", err)
continue
}
break
}
if err := mustClickPublishTab(page, "上传视频"); err != nil {
return nil, errors.Wrap(err, "切换到上传视频失败")
}
time.Sleep(1 * time.Second)