diff --git a/handlers_api.go b/handlers_api.go index 32a1f4f..7fbe2e1 100644 --- a/handlers_api.go +++ b/handlers_api.go @@ -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{ diff --git a/mcp_handlers.go b/mcp_handlers.go index ca1f38e..a9d5a07 100644 --- a/mcp_handlers.go +++ b/mcp_handlers.go @@ -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, + }}, + } +} diff --git a/routes.go b/routes.go index 75b85e2..bddabe2 100644 --- a/routes.go +++ b/routes.go @@ -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 diff --git a/service.go b/service.go index eeb49cd..4e72a3d 100644 --- a/service.go +++ b/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 +} diff --git a/streamable_http.go b/streamable_http.go index 6dbefce..e0a235b 100644 --- a/streamable_http.go +++ b/streamable_http.go @@ -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", diff --git a/types.go b/types.go index 381ba8c..4caf657 100644 --- a/types.go +++ b/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"` +} diff --git a/xiaohongshu/comment_feed.go b/xiaohongshu/comment_feed.go new file mode 100644 index 0000000..eb953e2 --- /dev/null +++ b/xiaohongshu/comment_feed.go @@ -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 +} diff --git a/xiaohongshu/feed_detail.go b/xiaohongshu/feed_detail.go index 899c47a..db8a08a 100644 --- a/xiaohongshu/feed_detail.go +++ b/xiaohongshu/feed_detail.go @@ -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) +}