fix get data panic (#244)

* fix: 修复 data 获取时的循环引用错误
This commit is contained in:
zy
2025-10-16 23:00:57 +08:00
committed by GitHub
parent 844ff8c102
commit df623caf18
8 changed files with 129 additions and 85 deletions

6
errors/errors.go Normal file
View File

@@ -0,0 +1,6 @@
package errors
import "errors"
var ErrNoFeeds = errors.New("没有捕获到 feeds 数据")
var ErrNoFeedDetail = errors.New("没有捕获到 feed 详情数据")

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"encoding/base64" "encoding/base64"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -177,7 +178,7 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
mcp.AddTool(server, mcp.AddTool(server,
&mcp.Tool{ &mcp.Tool{
Name: "user_profile", Name: "user_profile",
Description: "获取小红书用户主页,返回用户基本信息,关注、粉丝、获赞量及其笔记内容", Description: "获取指定的小红书用户主页,返回用户基本信息,关注、粉丝、获赞量及其笔记内容",
}, },
func(ctx context.Context, req *mcp.CallToolRequest, args UserProfileArgs) (*mcp.CallToolResult, any, error) { func(ctx context.Context, req *mcp.CallToolRequest, args UserProfileArgs) (*mcp.CallToolResult, any, error) {
argsMap := map[string]interface{}{ argsMap := map[string]interface{}{

View File

@@ -4,6 +4,9 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"time"
"github.com/go-rod/rod" "github.com/go-rod/rod"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@@ -13,8 +16,6 @@ import (
"github.com/xpzouying/xiaohongshu-mcp/cookies" "github.com/xpzouying/xiaohongshu-mcp/cookies"
"github.com/xpzouying/xiaohongshu-mcp/pkg/downloader" "github.com/xpzouying/xiaohongshu-mcp/pkg/downloader"
"github.com/xpzouying/xiaohongshu-mcp/xiaohongshu" "github.com/xpzouying/xiaohongshu-mcp/xiaohongshu"
"os"
"time"
) )
// XiaohongshuService 小红书业务服务 // XiaohongshuService 小红书业务服务
@@ -284,6 +285,7 @@ func (s *XiaohongshuService) ListFeeds(ctx context.Context) (*FeedsListResponse,
// 获取 Feeds 列表 // 获取 Feeds 列表
feeds, err := action.GetFeedsList(ctx) feeds, err := action.GetFeedsList(ctx)
if err != nil { if err != nil {
logrus.Errorf("获取 Feeds 列表失败: %v", err)
return nil, err return nil, err
} }

View File

@@ -7,6 +7,8 @@ import (
"time" "time"
"github.com/go-rod/rod" "github.com/go-rod/rod"
"github.com/sirupsen/logrus"
"github.com/xpzouying/xiaohongshu-mcp/errors"
) )
// FeedDetailAction 表示 Feed 详情页动作 // FeedDetailAction 表示 Feed 详情页动作
@@ -26,39 +28,37 @@ func (f *FeedDetailAction) GetFeedDetail(ctx context.Context, feedID, xsecToken
// 构建详情页 URL // 构建详情页 URL
url := makeFeedDetailURL(feedID, xsecToken) url := makeFeedDetailURL(feedID, xsecToken)
logrus.Infof("打开 feed 详情页: %s", url)
// 导航到详情页 // 导航到详情页
page.MustNavigate(url) page.MustNavigate(url)
page.MustWaitDOMStable() page.MustWaitDOMStable()
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
// 获取 window.__INITIAL_STATE__ 并转换为 JSON 字符串
result := page.MustEval(`() => { result := page.MustEval(`() => {
if (window.__INITIAL_STATE__) { if (window.__INITIAL_STATE__ &&
return JSON.stringify(window.__INITIAL_STATE__); window.__INITIAL_STATE__.note &&
window.__INITIAL_STATE__.note.noteDetailMap) {
const noteDetailMap = window.__INITIAL_STATE__.note.noteDetailMap;
return JSON.stringify(noteDetailMap);
} }
return ""; return "";
}`).String() }`).String()
if result == "" { if result == "" {
return nil, fmt.Errorf("__INITIAL_STATE__ not found") return nil, errors.ErrNoFeedDetail
} }
// 定义响应结构并直接反序列化 var noteDetailMap map[string]struct {
var initialState struct {
Note struct {
NoteDetailMap map[string]struct {
Note FeedDetail `json:"note"` Note FeedDetail `json:"note"`
Comments CommentList `json:"comments"` Comments CommentList `json:"comments"`
} `json:"noteDetailMap"`
} `json:"note"`
} }
if err := json.Unmarshal([]byte(result), &initialState); err != nil { if err := json.Unmarshal([]byte(result), &noteDetailMap); err != nil {
return nil, fmt.Errorf("failed to unmarshal __INITIAL_STATE__: %w", err) return nil, fmt.Errorf("failed to unmarshal noteDetailMap: %w", err)
} }
// 从 noteDetailMap 中获取对应 feedID 的数据 noteDetail, exists := noteDetailMap[feedID]
noteDetail, exists := initialState.Note.NoteDetailMap[feedID]
if !exists { if !exists {
return nil, fmt.Errorf("feed %s not found in noteDetailMap", feedID) return nil, fmt.Errorf("feed %s not found in noteDetailMap", feedID)
} }

View File

@@ -7,17 +7,13 @@ import (
"time" "time"
"github.com/go-rod/rod" "github.com/go-rod/rod"
"github.com/xpzouying/xiaohongshu-mcp/errors"
) )
type FeedsListAction struct { type FeedsListAction struct {
page *rod.Page page *rod.Page
} }
// FeedsResult 定义页面初始状态结构
type FeedsResult struct {
Feed FeedData `json:"feed"`
}
func NewFeedsListAction(page *rod.Page) *FeedsListAction { func NewFeedsListAction(page *rod.Page) *FeedsListAction {
pp := page.Timeout(60 * time.Second) pp := page.Timeout(60 * time.Second)
@@ -33,24 +29,27 @@ func (f *FeedsListAction) GetFeedsList(ctx context.Context) ([]Feed, error) {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
// 获取 window.__INITIAL_STATE__ 并转换为 JSON 字符串
result := page.MustEval(`() => { result := page.MustEval(`() => {
if (window.__INITIAL_STATE__) { if (window.__INITIAL_STATE__ &&
return JSON.stringify(window.__INITIAL_STATE__); window.__INITIAL_STATE__.feed &&
window.__INITIAL_STATE__.feed.feeds) {
const feeds = window.__INITIAL_STATE__.feed.feeds;
const feedsData = feeds.value !== undefined ? feeds.value : feeds._value;
if (feedsData) {
return JSON.stringify(feedsData);
}
} }
return ""; return "";
}`).String() }`).String()
if result == "" { if result == "" {
return nil, fmt.Errorf("__INITIAL_STATE__ not found") return nil, errors.ErrNoFeeds
} }
// 解析完整的 InitialState var feeds []Feed
var state FeedsResult if err := json.Unmarshal([]byte(result), &feeds); err != nil {
if err := json.Unmarshal([]byte(result), &state); err != nil { return nil, fmt.Errorf("failed to unmarshal feeds: %w", err)
return nil, fmt.Errorf("failed to unmarshal __INITIAL_STATE__: %w", err)
} }
// 返回 feed.feeds._value return feeds, nil
return state.Feed.Feeds.Value, nil
} }

View File

@@ -9,6 +9,7 @@ import (
"github.com/go-rod/rod" "github.com/go-rod/rod"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
myerrors "github.com/xpzouying/xiaohongshu-mcp/errors"
) )
// ActionResult 通用动作响应(点赞/收藏等) // ActionResult 通用动作响应(点赞/收藏等)
@@ -213,33 +214,33 @@ func (a *FavoriteAction) toggleFavorite(page *rod.Page, feedID string, targetCol
// getInteractState 从 __INITIAL_STATE__ 读取笔记的点赞/收藏状态 // getInteractState 从 __INITIAL_STATE__ 读取笔记的点赞/收藏状态
func (a *interactAction) getInteractState(page *rod.Page, feedID string) (liked bool, collected bool, err error) { func (a *interactAction) getInteractState(page *rod.Page, feedID string) (liked bool, collected bool, err error) {
result := page.MustEval(`() => { result := page.MustEval(`() => {
if (window.__INITIAL_STATE__) { if (window.__INITIAL_STATE__ &&
return JSON.stringify(window.__INITIAL_STATE__); window.__INITIAL_STATE__.note &&
window.__INITIAL_STATE__.note.noteDetailMap) {
return JSON.stringify(window.__INITIAL_STATE__.note.noteDetailMap);
} }
return ""; return "";
}`).String() }`).String()
if result == "" { if result == "" {
return false, false, fmt.Errorf("__INITIAL_STATE__ not found") return false, false, myerrors.ErrNoFeedDetail
} }
var state struct { // 直接解析为 noteDetailMap
Note struct { var noteDetailMap map[string]struct {
NoteDetailMap map[string]struct {
Note struct { Note struct {
InteractInfo struct { InteractInfo struct {
Liked bool `json:"liked"` Liked bool `json:"liked"`
Collected bool `json:"collected"` Collected bool `json:"collected"`
} `json:"interactInfo"` } `json:"interactInfo"`
} `json:"note"` } `json:"note"`
} `json:"noteDetailMap"`
} `json:"note"`
} }
if err := json.Unmarshal([]byte(result), &state); err != nil { if err := json.Unmarshal([]byte(result), &noteDetailMap); err != nil {
return false, false, errors.Wrap(err, "unmarshal __INITIAL_STATE__ failed") return false, false, errors.Wrap(err, "unmarshal noteDetailMap failed")
} }
detail, ok := state.Note.NoteDetailMap[feedID] detail, ok := noteDetailMap[feedID]
if !ok { if !ok {
return false, false, fmt.Errorf("feed %s not in noteDetailMap", feedID) return false, false, fmt.Errorf("feed %s not in noteDetailMap", feedID)
} }

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/go-rod/rod" "github.com/go-rod/rod"
"github.com/xpzouying/xiaohongshu-mcp/errors"
) )
type SearchResult struct { type SearchResult struct {
@@ -190,24 +191,29 @@ func (s *SearchAction) Search(ctx context.Context, keyword string, filters ...Fi
page.MustWait(`() => window.__INITIAL_STATE__ !== undefined`) page.MustWait(`() => window.__INITIAL_STATE__ !== undefined`)
} }
// 获取 window.__INITIAL_STATE__ 并转换为 JSON 字符串
result := page.MustEval(`() => { result := page.MustEval(`() => {
if (window.__INITIAL_STATE__) { if (window.__INITIAL_STATE__ &&
return JSON.stringify(window.__INITIAL_STATE__); window.__INITIAL_STATE__.search &&
window.__INITIAL_STATE__.search.feeds) {
const feeds = window.__INITIAL_STATE__.search.feeds;
const feedsData = feeds.value !== undefined ? feeds.value : feeds._value;
if (feedsData) {
return JSON.stringify(feedsData);
}
} }
return ""; return "";
}`).String() }`).String()
if result == "" { if result == "" {
return nil, fmt.Errorf("__INITIAL_STATE__ not found") return nil, errors.ErrNoFeeds
} }
var searchResult SearchResult var feeds []Feed
if err := json.Unmarshal([]byte(result), &searchResult); err != nil { if err := json.Unmarshal([]byte(result), &feeds); err != nil {
return nil, fmt.Errorf("failed to unmarshal __INITIAL_STATE__: %w", err) return nil, fmt.Errorf("failed to unmarshal feeds: %w", err)
} }
return searchResult.Search.Feeds.Value, nil return feeds, nil
} }
func makeSearchURL(keyword string) string { func makeSearchURL(keyword string) string {

View File

@@ -33,42 +33,71 @@ func (u *UserProfileAction) UserProfile(ctx context.Context, userID, xsecToken s
func (u *UserProfileAction) extractUserProfileData(page *rod.Page) (*UserProfileResponse, error) { func (u *UserProfileAction) extractUserProfileData(page *rod.Page) (*UserProfileResponse, error) {
page.MustWait(`() => window.__INITIAL_STATE__ !== undefined`) page.MustWait(`() => window.__INITIAL_STATE__ !== undefined`)
// 获取 window.__INITIAL_STATE__ 并转换为 JSON 字符串 userDataResult := page.MustEval(`() => {
result := page.MustEval(`() => { if (window.__INITIAL_STATE__ &&
if (window.__INITIAL_STATE__) { window.__INITIAL_STATE__.user &&
return JSON.stringify(window.__INITIAL_STATE__); window.__INITIAL_STATE__.user.userPageData) {
const userPageData = window.__INITIAL_STATE__.user.userPageData;
const data = userPageData.value !== undefined ? userPageData.value : userPageData._value;
if (data) {
return JSON.stringify(data);
}
} }
return ""; return "";
}`).String() }`).String()
if result == "" { if userDataResult == "" {
return nil, fmt.Errorf("__INITIAL_STATE__ not found") return nil, fmt.Errorf("user.userPageData.value not found in __INITIAL_STATE__")
} }
// 定义响应结构并直接反序列化
var initialState = struct { // 2. 获取用户帖子window.__INITIAL_STATE__.user.notes.value
User struct { notesResult := page.MustEval(`() => {
UserPageData UserPageData `json:"userPageData"` if (window.__INITIAL_STATE__ &&
Notes struct { window.__INITIAL_STATE__.user &&
Feeds [][]Feed `json:"_rawValue"` // 帖子为双重数组 window.__INITIAL_STATE__.user.notes) {
} `json:"notes"` const notes = window.__INITIAL_STATE__.user.notes;
} `json:"user"` // 优先使用 valuegetter如果不存在则使用 _value内部字段
}{} const data = notes.value !== undefined ? notes.value : notes._value;
if err := json.Unmarshal([]byte(result), &initialState); err != nil { if (data) {
return nil, fmt.Errorf("failed to unmarshal __INITIAL_STATE__: %w", err) return JSON.stringify(data);
} }
}
return "";
}`).String()
if notesResult == "" {
return nil, fmt.Errorf("user.notes.value not found in __INITIAL_STATE__")
}
// 解析用户信息
var userPageData struct {
Interactions []UserInteractions `json:"interactions"`
BasicInfo UserBasicInfo `json:"basicInfo"`
}
if err := json.Unmarshal([]byte(userDataResult), &userPageData); err != nil {
return nil, fmt.Errorf("failed to unmarshal userPageData: %w", err)
}
// 解析帖子数据(帖子为双重数组)
var notesFeeds [][]Feed
if err := json.Unmarshal([]byte(notesResult), &notesFeeds); err != nil {
return nil, fmt.Errorf("failed to unmarshal notes: %w", err)
}
// 组装响应
response := &UserProfileResponse{ response := &UserProfileResponse{
UserBasicInfo: initialState.User.UserPageData.RawValue.BasicInfo, UserBasicInfo: userPageData.BasicInfo,
Interactions: initialState.User.UserPageData.RawValue.Interactions, Interactions: userPageData.Interactions,
} }
// 添加用户贴子
for _, feeds := range initialState.User.Notes.Feeds { // 添加用户帖子(展平双重数组)
for _, feeds := range notesFeeds {
if len(feeds) != 0 { if len(feeds) != 0 {
response.Feeds = append(response.Feeds, feeds...) response.Feeds = append(response.Feeds, feeds...)
} }
} }
return response, nil return response, nil
} }
func makeUserProfileURL(userID, xsecToken string) string { func makeUserProfileURL(userID, xsecToken string) string {