diff --git a/handlers_api.go b/handlers_api.go index 7fbe2e1..8f0b227 100644 --- a/handlers_api.go +++ b/handlers_api.go @@ -124,6 +124,27 @@ func (s *AppServer) getFeedDetailHandler(c *gin.Context) { respondSuccess(c, result, "获取Feed详情成功") } +// userProfileHandler 用户主页 +func (s *AppServer) userProfileHandler(c *gin.Context) { + var req UserProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + respondError(c, http.StatusBadRequest, "INVALID_REQUEST", + "请求参数错误", err.Error()) + return + } + + // 获取用户信息 + result, err := s.xiaohongshuService.UserProfile(c.Request.Context(), req.UserID, req.XsecToken) + if err != nil { + respondError(c, http.StatusInternalServerError, "GET_USER_PROFILE_FAILED", + "获取用户主页失败", err.Error()) + return + } + + c.Set("account", "ai-report") + respondSuccess(c, map[string]any{"data": result}, "result.Message") +} + // postCommentHandler 发表评论到Feed func (s *AppServer) postCommentHandler(c *gin.Context) { var req PostCommentRequest diff --git a/mcp_handlers.go b/mcp_handlers.go index 867913c..8986876 100644 --- a/mcp_handlers.go +++ b/mcp_handlers.go @@ -233,6 +233,66 @@ func (s *AppServer) handleGetFeedDetail(ctx context.Context, args map[string]any } } +// 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), + }}, + } +} + // handlePostComment 处理发表评论到Feed func (s *AppServer) handlePostComment(ctx context.Context, args map[string]interface{}) *MCPToolResult { logrus.Info("MCP: 发表评论到Feed") diff --git a/routes.go b/routes.go index bddabe2..c476ae6 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("/user/profile", appServer.userProfileHandler) api.POST("/feeds/comment", appServer.postCommentHandler) } diff --git a/service.go b/service.go index d8d8589..3d02658 100644 --- a/service.go +++ b/service.go @@ -49,6 +49,13 @@ type FeedsListResponse struct { Count int `json:"count"` } +// UserProfileResponse 用户主页响应 +type UserProfileResponse struct { + UserBasicInfo xiaohongshu.UserBasicInfo `json:"userBasicInfo"` + Interactions []xiaohongshu.UserInteractions `json:"interactions"` + Feeds []xiaohongshu.Feed `json:"feeds"` +} + // CheckLoginStatus 检查登录状态 func (s *XiaohongshuService) CheckLoginStatus(ctx context.Context) (*LoginStatusResponse, error) { b := newBrowser() @@ -205,6 +212,30 @@ func (s *XiaohongshuService) GetFeedDetail(ctx context.Context, feedID, xsecToke return response, nil } +// UserProfile 获取用户信息 +func (s *XiaohongshuService) UserProfile(ctx context.Context, userID, xsecToken string) (*UserProfileResponse, error) { + b := newBrowser() + defer b.Close() + + page := b.NewPage() + defer page.Close() + + action := xiaohongshu.NewUserProfileAction(page) + + result, err := action.UserProfile(ctx, userID, xsecToken) + if err != nil { + return nil, err + } + response := &UserProfileResponse{ + UserBasicInfo: result.UserBasicInfo, + Interactions: result.Interactions, + Feeds: result.Feeds, + } + + return response, nil + +} + // PostCommentToFeed 发表评论到Feed func (s *XiaohongshuService) PostCommentToFeed(ctx context.Context, feedID, xsecToken, content string) (*PostCommentResponse, error) { // 使用非无头模式以便查看操作过程 diff --git a/streamable_http.go b/streamable_http.go index 0761504..5a8e212 100644 --- a/streamable_http.go +++ b/streamable_http.go @@ -237,6 +237,24 @@ func (s *AppServer) processToolsList(request *JSONRPCRequest) *JSONRPCResponse { "required": []string{"feed_id", "xsec_token"}, }, }, + { + "name": "user_profile", + "description": "获取小红书用户主页,返回用户基本信息,关注、粉丝、获赞量及其笔记内容", + "inputSchema": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "user_id": map[string]interface{}{ + "type": "string", + "description": "小红书用户ID,从Feed列表获取", + }, + "xsec_token": map[string]interface{}{ + "type": "string", + "description": "访问令牌,从Feed列表的xsecToken字段获取", + }, + }, + "required": []string{"user_id", "xsec_token"}, + }, + }, { "name": "post_comment_to_feed", "description": "发表评论到小红书笔记", @@ -301,6 +319,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 "user_profile": + result = s.handleUserProfile(ctx, toolArgs) case "post_comment_to_feed": result = s.handlePostComment(ctx, toolArgs) default: diff --git a/types.go b/types.go index 4caf657..ebecdf4 100644 --- a/types.go +++ b/types.go @@ -86,3 +86,9 @@ type PostCommentResponse struct { Success bool `json:"success"` Message string `json:"message"` } + +// UserProfileRequest 用户主页请求 +type UserProfileRequest struct { + UserID string `json:"user_id" binding:"required"` + XsecToken string `json:"xsec_token" binding:"required"` +} diff --git a/xiaohongshu/types.go b/xiaohongshu/types.go index 1ae35fe..bac0c70 100644 --- a/xiaohongshu/types.go +++ b/xiaohongshu/types.go @@ -135,3 +135,36 @@ type Comment struct { SubComments []Comment `json:"subComments"` ShowTags []string `json:"showTags"` } + +// UserProfileResponse 用户详情页完整响应 +type UserProfileResponse struct { + UserBasicInfo UserBasicInfo `json:"userBasicInfo"` + Interactions []UserInteractions `json:"interactions"` + Feeds []Feed `json:"feeds"` +} + +// UserPageData 用户的详细信息 +type UserPageData struct { + RawValue struct { + Interactions []UserInteractions `json:"interactions"` + BasicInfo UserBasicInfo `json:"basicInfo"` + } `json:"_rawValue"` +} + +// UserBasicInfo 用户的基本信息 +type UserBasicInfo struct { + Gender int `json:"gender"` + IpLocation string `json:"ipLocation"` + Desc string `json:"desc"` + Imageb string `json:"imageb"` + Nickname string `json:"nickname"` + Images string `json:"images"` + RedId string `json:"redId"` +} + +// UserInteractions 用户的 关注 粉丝 收藏量 +type UserInteractions struct { + Type string `json:"type"` // follows fans interaction + Name string `json:"name"` // 关注 粉丝 获赞与收藏 + Count string `json:"count"` // 数量 +} diff --git a/xiaohongshu/user_profile.go b/xiaohongshu/user_profile.go new file mode 100644 index 0000000..d60d3d8 --- /dev/null +++ b/xiaohongshu/user_profile.go @@ -0,0 +1,71 @@ +package xiaohongshu + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/go-rod/rod" +) + +type UserProfileAction struct { + page *rod.Page +} + +func NewUserProfileAction(page *rod.Page) *UserProfileAction { + pp := page.Timeout(60 * time.Second) + return &UserProfileAction{page: pp} +} + +// UserProfile 获取用户基本信息及帖子 +func (u *UserProfileAction) UserProfile(ctx context.Context, userID, xsecToken string) (*UserProfileResponse, error) { + page := u.page.Context(ctx) + + searchURL := makeUserProfileURL(userID, xsecToken) + page.MustNavigate(searchURL) + page.MustWaitStable() + + page.MustWait(`() => window.__INITIAL_STATE__ !== undefined`) + + // 获取 window.__INITIAL_STATE__ 并转换为 JSON 字符串 + result := page.MustEval(`() => { + if (window.__INITIAL_STATE__) { + return JSON.stringify(window.__INITIAL_STATE__); + } + return ""; + }`).String() + + if result == "" { + return nil, fmt.Errorf("__INITIAL_STATE__ not found") + } + // 定义响应结构并直接反序列化 + var initialState = struct { + User struct { + UserPageData UserPageData `json:"userPageData"` + Notes struct { + Feeds [][]Feed `json:"_rawValue"` // 帖子为双重数组 + } `json:"notes"` + } `json:"user"` + }{} + if err := json.Unmarshal([]byte(result), &initialState); err != nil { + return nil, fmt.Errorf("failed to unmarshal __INITIAL_STATE__: %w", err) + } + response := &UserProfileResponse{ + UserBasicInfo: initialState.User.UserPageData.RawValue.BasicInfo, + Interactions: initialState.User.UserPageData.RawValue.Interactions, + } + // 添加用户贴子 + for _, feeds := range initialState.User.Notes.Feeds { + if len(feeds) != 0 { + response.Feeds = append(response.Feeds, feeds...) + } + } + + return response, nil + +} + +func makeUserProfileURL(userID, xsecToken string) string { + return fmt.Sprintf("https://www.xiaohongshu.com/user/profile/%s?xsec_token=%s&xsec_source=pc_note", userID, xsecToken) +}