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详情成功")
|
||||
}
|
||||
|
||||
// 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 健康检查
|
||||
func healthHandler(c *gin.Context) {
|
||||
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/search", appServer.searchFeedsHandler)
|
||||
api.POST("/feeds/detail", appServer.getFeedDetailHandler)
|
||||
api.POST("/feeds/comment", appServer.postCommentHandler)
|
||||
}
|
||||
|
||||
return router
|
||||
|
||||
27
service.go
27
service.go
@@ -192,3 +192,30 @@ func (s *XiaohongshuService) GetFeedDetail(ctx context.Context, feedID, xsecToke
|
||||
|
||||
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"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"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{
|
||||
@@ -275,6 +297,8 @@ func (s *AppServer) processToolCall(ctx context.Context, request *JSONRPCRequest
|
||||
result = s.handleSearchFeeds(ctx, toolArgs)
|
||||
case "get_feed_detail":
|
||||
result = s.handleGetFeedDetail(ctx, toolArgs)
|
||||
case "post_comment_to_feed":
|
||||
result = s.handlePostComment(ctx, toolArgs)
|
||||
default:
|
||||
return &JSONRPCResponse{
|
||||
JSONRPC: "2.0",
|
||||
|
||||
14
types.go
14
types.go
@@ -72,3 +72,17 @@ type FeedDetailResponse struct {
|
||||
FeedID string `json:"feed_id"`
|
||||
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)
|
||||
|
||||
// 构建详情页 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)
|
||||
@@ -75,3 +75,7 @@ func (f *FeedDetailAction) GetFeedDetail(ctx context.Context, feedID, xsecToken
|
||||
Comments: noteDetail.Comments,
|
||||
}, 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