package main import ( "context" "encoding/json" "fmt" "strconv" "strings" "time" "github.com/sirupsen/logrus" "github.com/xpzouying/xiaohongshu-mcp/cookies" "github.com/xpzouying/xiaohongshu-mcp/xiaohongshu" ) // MCP 工具处理函数 // parseVisibility 从 MCP 参数中解析可见范围 func parseVisibility(args map[string]interface{}) string { v, ok := args["visibility"] if !ok || v == nil { return "" } if s, ok := v.(string); ok { return s } return "" } // handleCheckLoginStatus 处理检查登录状态 func (s *AppServer) handleCheckLoginStatus(ctx context.Context) *MCPToolResult { logrus.Info("MCP: 检查登录状态") status, err := s.xiaohongshuService.CheckLoginStatus(ctx) if err != nil { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "检查登录状态失败: " + err.Error(), }}, IsError: true, } } // 根据 IsLoggedIn 判断并返回友好的提示 var resultText string if status.IsLoggedIn { resultText = fmt.Sprintf("✅ 已登录\n用户名: %s\n\n你可以使用其他功能了。", status.Username) } else { resultText = fmt.Sprintf("❌ 未登录\n\n请使用 get_login_qrcode 工具获取二维码进行登录。") } return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: resultText, }}, } } // handleGetLoginQrcode 处理获取登录二维码请求。 // 返回二维码图片的 Base64 编码和超时时间,供前端展示扫码登录。 func (s *AppServer) handleGetLoginQrcode(ctx context.Context) *MCPToolResult { logrus.Info("MCP: 获取登录扫码图片") result, err := s.xiaohongshuService.GetLoginQrcode(ctx) if err != nil { return &MCPToolResult{ Content: []MCPContent{{Type: "text", Text: "获取登录扫码图片失败: " + err.Error()}}, IsError: true, } } if result.IsLoggedIn { return &MCPToolResult{ Content: []MCPContent{{Type: "text", Text: "你当前已处于登录状态"}}, } } now := time.Now() deadline := func() string { d, err := time.ParseDuration(result.Timeout) if err != nil { return now.Format("2006-01-02 15:04:05") } return now.Add(d).Format("2006-01-02 15:04:05") }() // 已登录:文本 + 图片 contents := []MCPContent{ {Type: "text", Text: "请用小红书 App 在 " + deadline + " 前扫码登录 👇"}, { Type: "image", MimeType: "image/png", Data: strings.TrimPrefix(result.Img, "data:image/png;base64,"), }, } return &MCPToolResult{Content: contents} } // handleDeleteCookies 处理删除 cookies 请求,用于登录重置 func (s *AppServer) handleDeleteCookies(ctx context.Context) *MCPToolResult { logrus.Info("MCP: 删除 cookies,重置登录状态") err := s.xiaohongshuService.DeleteCookies(ctx) if err != nil { return &MCPToolResult{ Content: []MCPContent{{Type: "text", Text: "删除 cookies 失败: " + err.Error()}}, IsError: true, } } cookiePath := cookies.GetCookiesFilePath() resultText := fmt.Sprintf("Cookies 已成功删除,登录状态已重置。\n\n删除的文件路径: %s\n\n下次操作时,需要重新登录。", cookiePath) return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: resultText, }}, } } // handlePublishContent 处理发布内容 func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]interface{}) *MCPToolResult { logrus.Info("MCP: 发布内容") // 解析参数 title, _ := args["title"].(string) content, _ := args["content"].(string) imagePathsInterface, _ := args["images"].([]interface{}) tagsInterface, _ := args["tags"].([]interface{}) var imagePaths []string for _, path := range imagePathsInterface { if pathStr, ok := path.(string); ok { imagePaths = append(imagePaths, pathStr) } } var tags []string for _, tag := range tagsInterface { if tagStr, ok := tag.(string); ok { tags = append(tags, tagStr) } } // 解析定时发布参数 scheduleAt, _ := args["schedule_at"].(string) visibility := parseVisibility(args) // 解析原创参数 isOriginal, _ := args["is_original"].(bool) logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d, 标签数量: %d, 定时: %s, 原创: %v, visibility: %s", title, len(imagePaths), len(tags), scheduleAt, isOriginal, visibility) // 构建发布请求 req := &PublishRequest{ Title: title, Content: content, Images: imagePaths, Tags: tags, ScheduleAt: scheduleAt, IsOriginal: isOriginal, Visibility: visibility, } // 执行发布 result, err := s.xiaohongshuService.PublishContent(ctx, req) if err != nil { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "发布失败: " + err.Error(), }}, IsError: true, } } resultText := fmt.Sprintf("内容发布成功: %+v", result) return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: resultText, }}, } } // handlePublishVideo 处理发布视频内容(仅本地单个视频文件) func (s *AppServer) handlePublishVideo(ctx context.Context, args map[string]interface{}) *MCPToolResult { logrus.Info("MCP: 发布视频内容(本地)") title, _ := args["title"].(string) content, _ := args["content"].(string) videoPath, _ := args["video"].(string) tagsInterface, _ := args["tags"].([]interface{}) var tags []string for _, tag := range tagsInterface { if tagStr, ok := tag.(string); ok { tags = append(tags, tagStr) } } if videoPath == "" { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "发布失败: 缺少本地视频文件路径", }}, IsError: true, } } // 解析定时发布参数 scheduleAt, _ := args["schedule_at"].(string) visibility := parseVisibility(args) logrus.Infof("MCP: 发布视频 - 标题: %s, 标签数量: %d, 定时: %s, visibility: %s", title, len(tags), scheduleAt, visibility) // 构建发布请求 req := &PublishVideoRequest{ Title: title, Content: content, Video: videoPath, Tags: tags, ScheduleAt: scheduleAt, Visibility: visibility, } // 执行发布 result, err := s.xiaohongshuService.PublishVideo(ctx, req) if err != nil { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "发布失败: " + err.Error(), }}, IsError: true, } } resultText := fmt.Sprintf("视频发布成功: %+v", result) return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: resultText, }}, } } // handleListFeeds 处理获取Feeds列表 func (s *AppServer) handleListFeeds(ctx context.Context) *MCPToolResult { logrus.Info("MCP: 获取Feeds列表") result, err := s.xiaohongshuService.ListFeeds(ctx) if err != nil { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "获取Feeds列表失败: " + err.Error(), }}, IsError: true, } } // 格式化输出,转换为JSON字符串 jsonData, err := json.MarshalIndent(result, "", " ") if err != nil { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: fmt.Sprintf("获取Feeds列表成功,但序列化失败: %v", err), }}, IsError: true, } } return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: string(jsonData), }}, } } // handleSearchFeeds 处理搜索Feeds func (s *AppServer) handleSearchFeeds(ctx context.Context, args SearchFeedsArgs) *MCPToolResult { logrus.Info("MCP: 搜索Feeds") if args.Keyword == "" { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "搜索Feeds失败: 缺少关键词参数", }}, IsError: true, } } logrus.Infof("MCP: 搜索Feeds - 关键词: %s", args.Keyword) // 将 MCP 的 FilterOption 转换为 xiaohongshu.FilterOption filter := xiaohongshu.FilterOption{ SortBy: args.Filters.SortBy, NoteType: args.Filters.NoteType, PublishTime: args.Filters.PublishTime, SearchScope: args.Filters.SearchScope, Location: args.Filters.Location, } result, err := s.xiaohongshuService.SearchFeeds(ctx, args.Keyword, filter) if err != nil { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "搜索Feeds失败: " + err.Error(), }}, IsError: true, } } // 格式化输出,转换为JSON字符串 jsonData, err := json.MarshalIndent(result, "", " ") if err != nil { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: fmt.Sprintf("搜索Feeds成功,但序列化失败: %v", err), }}, IsError: true, } } return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: string(jsonData), }}, } } // handleGetFeedDetail 处理获取Feed详情 func (s *AppServer) handleGetFeedDetail(ctx context.Context, args map[string]any) *MCPToolResult { logrus.Info("MCP: 获取Feed详情") // 解析参数 feedID, ok := args["feed_id"].(string) if !ok || feedID == "" { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "获取Feed详情失败: 缺少feed_id参数", }}, IsError: true, } } xsecToken, ok := args["xsec_token"].(string) if !ok || xsecToken == "" { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "获取Feed详情失败: 缺少xsec_token参数", }}, IsError: true, } } loadAll := false if raw, ok := args["load_all_comments"]; ok { switch v := raw.(type) { case bool: loadAll = v case string: if parsed, err := strconv.ParseBool(v); err == nil { loadAll = parsed } case float64: loadAll = v != 0 } } // 解析评论配置参数,如果未提供则使用默认值 config := xiaohongshu.DefaultCommentLoadConfig() if raw, ok := args["click_more_replies"]; ok { switch v := raw.(type) { case bool: config.ClickMoreReplies = v case string: if parsed, err := strconv.ParseBool(v); err == nil { config.ClickMoreReplies = parsed } } } if raw, ok := args["max_replies_threshold"]; ok { switch v := raw.(type) { case float64: config.MaxRepliesThreshold = int(v) case string: if parsed, err := strconv.Atoi(v); err == nil { config.MaxRepliesThreshold = parsed } case int: config.MaxRepliesThreshold = v } } if raw, ok := args["max_comment_items"]; ok { switch v := raw.(type) { case float64: config.MaxCommentItems = int(v) case string: if parsed, err := strconv.Atoi(v); err == nil { config.MaxCommentItems = parsed } case int: config.MaxCommentItems = v } } if raw, ok := args["scroll_speed"].(string); ok && raw != "" { config.ScrollSpeed = raw } logrus.Infof("MCP: 获取Feed详情 - Feed ID: %s, loadAllComments=%v, config=%+v", feedID, loadAll, config) result, err := s.xiaohongshuService.GetFeedDetailWithConfig(ctx, feedID, xsecToken, loadAll, config) if err != nil { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "获取Feed详情失败: " + err.Error(), }}, IsError: true, } } // 格式化输出,转换为JSON字符串 jsonData, err := json.MarshalIndent(result, "", " ") if err != nil { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: fmt.Sprintf("获取Feed详情成功,但序列化失败: %v", err), }}, IsError: true, } } return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: string(jsonData), }}, } } // handleUserProfile 获取用户主页 func (s *AppServer) handleUserProfile(ctx context.Context, args map[string]any) *MCPToolResult { logrus.Info("MCP: 获取用户主页") // 解析参数 userID, ok := args["user_id"].(string) if !ok || userID == "" { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "获取用户主页失败: 缺少user_id参数", }}, IsError: true, } } xsecToken, ok := args["xsec_token"].(string) if !ok || xsecToken == "" { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "获取用户主页失败: 缺少xsec_token参数", }}, IsError: true, } } logrus.Infof("MCP: 获取用户主页 - User ID: %s", userID) result, err := s.xiaohongshuService.UserProfile(ctx, userID, xsecToken) if err != nil { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "获取用户主页失败: " + err.Error(), }}, IsError: true, } } // 格式化输出,转换为JSON字符串 jsonData, err := json.MarshalIndent(result, "", " ") if err != nil { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: fmt.Sprintf("获取用户主页,但序列化失败: %v", err), }}, IsError: true, } } return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: string(jsonData), }}, } } // handleLikeFeed 处理点赞/取消点赞 func (s *AppServer) handleLikeFeed(ctx context.Context, args map[string]interface{}) *MCPToolResult { 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} } unlike, _ := args["unlike"].(bool) var res *ActionResult var err error if unlike { res, err = s.xiaohongshuService.UnlikeFeed(ctx, feedID, xsecToken) } else { res, err = s.xiaohongshuService.LikeFeed(ctx, feedID, xsecToken) } if err != nil { action := "点赞" if unlike { action = "取消点赞" } return &MCPToolResult{Content: []MCPContent{{Type: "text", Text: action + "失败: " + err.Error()}}, IsError: true} } action := "点赞" if unlike { action = "取消点赞" } return &MCPToolResult{Content: []MCPContent{{Type: "text", Text: fmt.Sprintf("%s成功 - Feed ID: %s", action, res.FeedID)}}} } // handleFavoriteFeed 处理收藏/取消收藏 func (s *AppServer) handleFavoriteFeed(ctx context.Context, args map[string]interface{}) *MCPToolResult { 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} } unfavorite, _ := args["unfavorite"].(bool) var res *ActionResult var err error if unfavorite { res, err = s.xiaohongshuService.UnfavoriteFeed(ctx, feedID, xsecToken) } else { res, err = s.xiaohongshuService.FavoriteFeed(ctx, feedID, xsecToken) } if err != nil { action := "收藏" if unfavorite { action = "取消收藏" } return &MCPToolResult{Content: []MCPContent{{Type: "text", Text: action + "失败: " + err.Error()}}, IsError: true} } action := "收藏" if unfavorite { action = "取消收藏" } return &MCPToolResult{Content: []MCPContent{{Type: "text", Text: fmt.Sprintf("%s成功 - Feed ID: %s", action, res.FeedID)}}} } // 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, }}, } } // handleReplyComment 处理回复评论 func (s *AppServer) handleReplyComment(ctx context.Context, args map[string]interface{}) *MCPToolResult { logrus.Info("MCP: 回复评论") // 解析参数 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, } } commentID, _ := args["comment_id"].(string) userID, _ := args["user_id"].(string) if commentID == "" && userID == "" { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "回复评论失败: 缺少comment_id或user_id参数", }}, 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, Comment ID: %s, User ID: %s, 内容长度: %d", feedID, commentID, userID, len(content)) // 回复评论 result, err := s.xiaohongshuService.ReplyCommentToFeed(ctx, feedID, xsecToken, commentID, userID, content) if err != nil { return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: "回复评论失败: " + err.Error(), }}, IsError: true, } } // 返回成功结果 responseText := fmt.Sprintf("评论回复成功 - Feed ID: %s, Comment ID: %s, User ID: %s", result.FeedID, result.TargetCommentID, result.TargetUserID) return &MCPToolResult{ Content: []MCPContent{{ Type: "text", Text: responseText, }}, } }