@@ -124,6 +124,27 @@ func (s *AppServer) getFeedDetailHandler(c *gin.Context) {
|
|||||||
respondSuccess(c, result, "获取Feed详情成功")
|
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
|
// postCommentHandler 发表评论到Feed
|
||||||
func (s *AppServer) postCommentHandler(c *gin.Context) {
|
func (s *AppServer) postCommentHandler(c *gin.Context) {
|
||||||
var req PostCommentRequest
|
var req PostCommentRequest
|
||||||
|
|||||||
@@ -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
|
// handlePostComment 处理发表评论到Feed
|
||||||
func (s *AppServer) handlePostComment(ctx context.Context, args map[string]interface{}) *MCPToolResult {
|
func (s *AppServer) handlePostComment(ctx context.Context, args map[string]interface{}) *MCPToolResult {
|
||||||
logrus.Info("MCP: 发表评论到Feed")
|
logrus.Info("MCP: 发表评论到Feed")
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ func setupRoutes(appServer *AppServer) *gin.Engine {
|
|||||||
api.GET("/feeds/list", appServer.listFeedsHandler)
|
api.GET("/feeds/list", appServer.listFeedsHandler)
|
||||||
api.GET("/feeds/search", appServer.searchFeedsHandler)
|
api.GET("/feeds/search", appServer.searchFeedsHandler)
|
||||||
api.POST("/feeds/detail", appServer.getFeedDetailHandler)
|
api.POST("/feeds/detail", appServer.getFeedDetailHandler)
|
||||||
|
api.POST("/user/profile", appServer.userProfileHandler)
|
||||||
api.POST("/feeds/comment", appServer.postCommentHandler)
|
api.POST("/feeds/comment", appServer.postCommentHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
31
service.go
31
service.go
@@ -49,6 +49,13 @@ type FeedsListResponse struct {
|
|||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserProfileResponse 用户主页响应
|
||||||
|
type UserProfileResponse struct {
|
||||||
|
UserBasicInfo xiaohongshu.UserBasicInfo `json:"userBasicInfo"`
|
||||||
|
Interactions []xiaohongshu.UserInteractions `json:"interactions"`
|
||||||
|
Feeds []xiaohongshu.Feed `json:"feeds"`
|
||||||
|
}
|
||||||
|
|
||||||
// CheckLoginStatus 检查登录状态
|
// CheckLoginStatus 检查登录状态
|
||||||
func (s *XiaohongshuService) CheckLoginStatus(ctx context.Context) (*LoginStatusResponse, error) {
|
func (s *XiaohongshuService) CheckLoginStatus(ctx context.Context) (*LoginStatusResponse, error) {
|
||||||
b := newBrowser()
|
b := newBrowser()
|
||||||
@@ -205,6 +212,30 @@ func (s *XiaohongshuService) GetFeedDetail(ctx context.Context, feedID, xsecToke
|
|||||||
return response, nil
|
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
|
// PostCommentToFeed 发表评论到Feed
|
||||||
func (s *XiaohongshuService) PostCommentToFeed(ctx context.Context, feedID, xsecToken, content string) (*PostCommentResponse, error) {
|
func (s *XiaohongshuService) PostCommentToFeed(ctx context.Context, feedID, xsecToken, content string) (*PostCommentResponse, error) {
|
||||||
// 使用非无头模式以便查看操作过程
|
// 使用非无头模式以便查看操作过程
|
||||||
|
|||||||
@@ -237,6 +237,24 @@ func (s *AppServer) processToolsList(request *JSONRPCRequest) *JSONRPCResponse {
|
|||||||
"required": []string{"feed_id", "xsec_token"},
|
"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",
|
"name": "post_comment_to_feed",
|
||||||
"description": "发表评论到小红书笔记",
|
"description": "发表评论到小红书笔记",
|
||||||
@@ -301,6 +319,8 @@ func (s *AppServer) processToolCall(ctx context.Context, request *JSONRPCRequest
|
|||||||
result = s.handleSearchFeeds(ctx, toolArgs)
|
result = s.handleSearchFeeds(ctx, toolArgs)
|
||||||
case "get_feed_detail":
|
case "get_feed_detail":
|
||||||
result = s.handleGetFeedDetail(ctx, toolArgs)
|
result = s.handleGetFeedDetail(ctx, toolArgs)
|
||||||
|
case "user_profile":
|
||||||
|
result = s.handleUserProfile(ctx, toolArgs)
|
||||||
case "post_comment_to_feed":
|
case "post_comment_to_feed":
|
||||||
result = s.handlePostComment(ctx, toolArgs)
|
result = s.handlePostComment(ctx, toolArgs)
|
||||||
default:
|
default:
|
||||||
|
|||||||
6
types.go
6
types.go
@@ -86,3 +86,9 @@ type PostCommentResponse struct {
|
|||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserProfileRequest 用户主页请求
|
||||||
|
type UserProfileRequest struct {
|
||||||
|
UserID string `json:"user_id" binding:"required"`
|
||||||
|
XsecToken string `json:"xsec_token" binding:"required"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -135,3 +135,36 @@ type Comment struct {
|
|||||||
SubComments []Comment `json:"subComments"`
|
SubComments []Comment `json:"subComments"`
|
||||||
ShowTags []string `json:"showTags"`
|
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"` // 数量
|
||||||
|
}
|
||||||
|
|||||||
71
xiaohongshu/user_profile.go
Normal file
71
xiaohongshu/user_profile.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user