feat: 添加小红书Feed评论功能 (#50)
实现通过HTTP GIN API和MCP API发表评论到小红书Feed的功能: - 新增POST /api/v1/feeds/comment端点 - 新增post_comment_to_feed MCP工具 - 添加PostCommentRequest和PostCommentResponse类型 - 实现PostCommentToFeed服务方法 - 新增CommentFeedAction用于浏览器自动化操作 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,27 @@ func (s *AppServer) getFeedDetailHandler(c *gin.Context) {
|
|||||||
respondSuccess(c, result, "获取Feed详情成功")
|
respondSuccess(c, result, "获取Feed详情成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// postCommentHandler 发表评论到Feed
|
||||||
|
func (s *AppServer) postCommentHandler(c *gin.Context) {
|
||||||
|
var req PostCommentRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
respondError(c, http.StatusBadRequest, "INVALID_REQUEST",
|
||||||
|
"请求参数错误", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发表评论
|
||||||
|
result, err := s.xiaohongshuService.PostCommentToFeed(c.Request.Context(), req.FeedID, req.XsecToken, req.Content)
|
||||||
|
if err != nil {
|
||||||
|
respondError(c, http.StatusInternalServerError, "POST_COMMENT_FAILED",
|
||||||
|
"发表评论失败", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("account", "ai-report")
|
||||||
|
respondSuccess(c, result, result.Message)
|
||||||
|
}
|
||||||
|
|
||||||
// healthHandler 健康检查
|
// healthHandler 健康检查
|
||||||
func healthHandler(c *gin.Context) {
|
func healthHandler(c *gin.Context) {
|
||||||
respondSuccess(c, map[string]any{
|
respondSuccess(c, map[string]any{
|
||||||
|
|||||||
@@ -223,3 +223,65 @@ func (s *AppServer) handleGetFeedDetail(ctx context.Context, args map[string]any
|
|||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handlePostComment 处理发表评论到Feed
|
||||||
|
func (s *AppServer) handlePostComment(ctx context.Context, args map[string]interface{}) *MCPToolResult {
|
||||||
|
logrus.Info("MCP: 发表评论到Feed")
|
||||||
|
|
||||||
|
// 解析参数
|
||||||
|
feedID, ok := args["feed_id"].(string)
|
||||||
|
if !ok || feedID == "" {
|
||||||
|
return &MCPToolResult{
|
||||||
|
Content: []MCPContent{{
|
||||||
|
Type: "text",
|
||||||
|
Text: "发表评论失败: 缺少feed_id参数",
|
||||||
|
}},
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xsecToken, ok := args["xsec_token"].(string)
|
||||||
|
if !ok || xsecToken == "" {
|
||||||
|
return &MCPToolResult{
|
||||||
|
Content: []MCPContent{{
|
||||||
|
Type: "text",
|
||||||
|
Text: "发表评论失败: 缺少xsec_token参数",
|
||||||
|
}},
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content, ok := args["content"].(string)
|
||||||
|
if !ok || content == "" {
|
||||||
|
return &MCPToolResult{
|
||||||
|
Content: []MCPContent{{
|
||||||
|
Type: "text",
|
||||||
|
Text: "发表评论失败: 缺少content参数",
|
||||||
|
}},
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("MCP: 发表评论 - Feed ID: %s, 内容长度: %d", feedID, len(content))
|
||||||
|
|
||||||
|
// 发表评论
|
||||||
|
result, err := s.xiaohongshuService.PostCommentToFeed(ctx, feedID, xsecToken, content)
|
||||||
|
if err != nil {
|
||||||
|
return &MCPToolResult{
|
||||||
|
Content: []MCPContent{{
|
||||||
|
Type: "text",
|
||||||
|
Text: "发表评论失败: " + err.Error(),
|
||||||
|
}},
|
||||||
|
IsError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回成功结果,只包含feed_id
|
||||||
|
resultText := fmt.Sprintf("评论发表成功 - Feed ID: %s", result.FeedID)
|
||||||
|
return &MCPToolResult{
|
||||||
|
Content: []MCPContent{{
|
||||||
|
Type: "text",
|
||||||
|
Text: resultText,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ func setupRoutes(appServer *AppServer) *gin.Engine {
|
|||||||
api.GET("/feeds/list", appServer.listFeedsHandler)
|
api.GET("/feeds/list", appServer.listFeedsHandler)
|
||||||
api.GET("/feeds/search", appServer.searchFeedsHandler)
|
api.GET("/feeds/search", appServer.searchFeedsHandler)
|
||||||
api.POST("/feeds/detail", appServer.getFeedDetailHandler)
|
api.POST("/feeds/detail", appServer.getFeedDetailHandler)
|
||||||
|
api.POST("/feeds/comment", appServer.postCommentHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|||||||
27
service.go
27
service.go
@@ -192,3 +192,30 @@ func (s *XiaohongshuService) GetFeedDetail(ctx context.Context, feedID, xsecToke
|
|||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PostCommentToFeed 发表评论到Feed
|
||||||
|
func (s *XiaohongshuService) PostCommentToFeed(ctx context.Context, feedID, xsecToken, content string) (*PostCommentResponse, error) {
|
||||||
|
// 使用非无头模式以便查看操作过程
|
||||||
|
b := browser.NewBrowser(false)
|
||||||
|
defer b.Close()
|
||||||
|
|
||||||
|
page := b.NewPage()
|
||||||
|
defer page.Close()
|
||||||
|
|
||||||
|
// 创建 Feed 评论 action
|
||||||
|
action := xiaohongshu.NewCommentFeedAction(page)
|
||||||
|
|
||||||
|
// 发表评论
|
||||||
|
err := action.PostComment(ctx, feedID, xsecToken, content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &PostCommentResponse{
|
||||||
|
FeedID: feedID,
|
||||||
|
Success: true,
|
||||||
|
Message: "评论发表成功",
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -233,6 +233,28 @@ func (s *AppServer) processToolsList(request *JSONRPCRequest) *JSONRPCResponse {
|
|||||||
"required": []string{"feed_id", "xsec_token"},
|
"required": []string{"feed_id", "xsec_token"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "post_comment_to_feed",
|
||||||
|
"description": "发表评论到小红书笔记",
|
||||||
|
"inputSchema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"feed_id": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "小红书笔记ID,从Feed列表获取",
|
||||||
|
},
|
||||||
|
"xsec_token": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "访问令牌,从Feed列表的xsecToken字段获取",
|
||||||
|
},
|
||||||
|
"content": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "评论内容",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"feed_id", "xsec_token", "content"},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return &JSONRPCResponse{
|
return &JSONRPCResponse{
|
||||||
@@ -275,6 +297,8 @@ func (s *AppServer) processToolCall(ctx context.Context, request *JSONRPCRequest
|
|||||||
result = s.handleSearchFeeds(ctx, toolArgs)
|
result = s.handleSearchFeeds(ctx, toolArgs)
|
||||||
case "get_feed_detail":
|
case "get_feed_detail":
|
||||||
result = s.handleGetFeedDetail(ctx, toolArgs)
|
result = s.handleGetFeedDetail(ctx, toolArgs)
|
||||||
|
case "post_comment_to_feed":
|
||||||
|
result = s.handlePostComment(ctx, toolArgs)
|
||||||
default:
|
default:
|
||||||
return &JSONRPCResponse{
|
return &JSONRPCResponse{
|
||||||
JSONRPC: "2.0",
|
JSONRPC: "2.0",
|
||||||
|
|||||||
14
types.go
14
types.go
@@ -72,3 +72,17 @@ type FeedDetailResponse struct {
|
|||||||
FeedID string `json:"feed_id"`
|
FeedID string `json:"feed_id"`
|
||||||
Data any `json:"data"`
|
Data any `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PostCommentRequest 发表评论请求
|
||||||
|
type PostCommentRequest struct {
|
||||||
|
FeedID string `json:"feed_id" binding:"required"`
|
||||||
|
XsecToken string `json:"xsec_token" binding:"required"`
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostCommentResponse 发表评论响应
|
||||||
|
type PostCommentResponse struct {
|
||||||
|
FeedID string `json:"feed_id"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|||||||
50
xiaohongshu/comment_feed.go
Normal file
50
xiaohongshu/comment_feed.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package xiaohongshu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-rod/rod"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommentFeedAction 表示 Feed 评论动作
|
||||||
|
type CommentFeedAction struct {
|
||||||
|
page *rod.Page
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommentFeedAction 创建 Feed 评论动作
|
||||||
|
func NewCommentFeedAction(page *rod.Page) *CommentFeedAction {
|
||||||
|
return &CommentFeedAction{page: page}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostComment 发表评论到 Feed
|
||||||
|
func (f *CommentFeedAction) PostComment(ctx context.Context, feedID, xsecToken, content string) error {
|
||||||
|
page := f.page.Context(ctx).Timeout(60 * time.Second)
|
||||||
|
|
||||||
|
// 构建详情页 URL
|
||||||
|
url := makeFeedDetailURL(feedID, xsecToken)
|
||||||
|
|
||||||
|
logrus.Infof("Opening feed detail page: %s", url)
|
||||||
|
|
||||||
|
// 导航到详情页
|
||||||
|
page.MustNavigate(url)
|
||||||
|
page.MustWaitDOMStable()
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
elem := page.MustElement("div.input-box div.content-edit span")
|
||||||
|
elem.MustClick()
|
||||||
|
|
||||||
|
elem2 := page.MustElement("div.input-box div.content-edit p.content-input")
|
||||||
|
elem2.MustInput(content)
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
submitButton := page.MustElement("div.bottom button.submit")
|
||||||
|
submitButton.MustClick()
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ func (f *FeedDetailAction) GetFeedDetail(ctx context.Context, feedID, xsecToken
|
|||||||
page := f.page.Context(ctx).Timeout(60 * time.Second)
|
page := f.page.Context(ctx).Timeout(60 * time.Second)
|
||||||
|
|
||||||
// 构建详情页 URL
|
// 构建详情页 URL
|
||||||
url := fmt.Sprintf("https://www.xiaohongshu.com/explore/%s?xsec_token=%s&xsec_source=pc_feed", feedID, xsecToken)
|
url := makeFeedDetailURL(feedID, xsecToken)
|
||||||
|
|
||||||
// 导航到详情页
|
// 导航到详情页
|
||||||
page.MustNavigate(url)
|
page.MustNavigate(url)
|
||||||
@@ -75,3 +75,7 @@ func (f *FeedDetailAction) GetFeedDetail(ctx context.Context, feedID, xsecToken
|
|||||||
Comments: noteDetail.Comments,
|
Comments: noteDetail.Comments,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeFeedDetailURL(feedID, xsecToken string) string {
|
||||||
|
return fmt.Sprintf("https://www.xiaohongshu.com/explore/%s?xsec_token=%s&xsec_source=pc_feed", feedID, xsecToken)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user