- Enhanced GetFeedDetail method to support loading all comments based on the new loadAllComments parameter. - Updated related handlers and request structures to accommodate the new parameter. - Improved logging to reflect the loading of all comments during feed detail retrieval. - Implemented JavaScript logic to scroll and collect comments when loadAllComments is true.
625 lines
15 KiB
Go
625 lines
15 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/sirupsen/logrus"
|
||
"github.com/xpzouying/xiaohongshu-mcp/xiaohongshu"
|
||
)
|
||
|
||
// MCP 工具处理函数
|
||
|
||
// 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,
|
||
}
|
||
}
|
||
|
||
resultText := fmt.Sprintf("登录状态检查成功: %+v", status)
|
||
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}
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
}
|
||
|
||
logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d, 标签数量: %d", title, len(imagePaths), len(tags))
|
||
|
||
// 构建发布请求
|
||
req := &PublishRequest{
|
||
Title: title,
|
||
Content: content,
|
||
Images: imagePaths,
|
||
Tags: tags,
|
||
}
|
||
|
||
// 执行发布
|
||
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,
|
||
}
|
||
}
|
||
|
||
logrus.Infof("MCP: 发布视频 - 标题: %s, 标签数量: %d", title, len(tags))
|
||
|
||
// 构建发布请求
|
||
req := &PublishVideoRequest{
|
||
Title: title,
|
||
Content: content,
|
||
Video: videoPath,
|
||
Tags: tags,
|
||
}
|
||
|
||
// 执行发布
|
||
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
|
||
}
|
||
}
|
||
|
||
logrus.Infof("MCP: 获取Feed详情 - Feed ID: %s, loadAllComments=%v", feedID, loadAll)
|
||
|
||
result, err := s.xiaohongshuService.GetFeedDetail(ctx, feedID, xsecToken, loadAll)
|
||
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,
|
||
}},
|
||
}
|
||
}
|