Skip to content

Commit df623ca

Browse files
authored
fix get data panic (#244)
* fix: 修复 data 获取时的循环引用错误
1 parent 844ff8c commit df623ca

File tree

8 files changed

+129
-85
lines changed

8 files changed

+129
-85
lines changed

errors/errors.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package errors
2+
3+
import "errors"
4+
5+
var ErrNoFeeds = errors.New("没有捕获到 feeds 数据")
6+
var ErrNoFeedDetail = errors.New("没有捕获到 feed 详情数据")

mcp_server.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"encoding/base64"
6+
67
"github.com/modelcontextprotocol/go-sdk/mcp"
78
"github.com/sirupsen/logrus"
89
)
@@ -64,8 +65,8 @@ type LikeFeedArgs struct {
6465

6566
// FavoriteFeedArgs 收藏参数
6667
type FavoriteFeedArgs struct {
67-
FeedID string `json:"feed_id" jsonschema:"小红书笔记ID,从Feed列表获取"`
68-
XsecToken string `json:"xsec_token" jsonschema:"访问令牌,从Feed列表的xsecToken字段获取"`
68+
FeedID string `json:"feed_id" jsonschema:"小红书笔记ID,从Feed列表获取"`
69+
XsecToken string `json:"xsec_token" jsonschema:"访问令牌,从Feed列表的xsecToken字段获取"`
6970
Unfavorite bool `json:"unfavorite,omitempty" jsonschema:"是否取消收藏,true为取消收藏,false或未设置则为收藏"`
7071
}
7172

@@ -177,7 +178,7 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
177178
mcp.AddTool(server,
178179
&mcp.Tool{
179180
Name: "user_profile",
180-
Description: "获取小红书用户主页,返回用户基本信息,关注、粉丝、获赞量及其笔记内容",
181+
Description: "获取指定的小红书用户主页,返回用户基本信息,关注、粉丝、获赞量及其笔记内容",
181182
},
182183
func(ctx context.Context, req *mcp.CallToolRequest, args UserProfileArgs) (*mcp.CallToolResult, any, error) {
183184
argsMap := map[string]interface{}{

service.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"os"
8+
"time"
9+
710
"github.com/go-rod/rod"
811
"github.com/mattn/go-runewidth"
912
"github.com/sirupsen/logrus"
@@ -13,8 +16,6 @@ import (
1316
"github.com/xpzouying/xiaohongshu-mcp/cookies"
1417
"github.com/xpzouying/xiaohongshu-mcp/pkg/downloader"
1518
"github.com/xpzouying/xiaohongshu-mcp/xiaohongshu"
16-
"os"
17-
"time"
1819
)
1920

2021
// XiaohongshuService 小红书业务服务
@@ -284,6 +285,7 @@ func (s *XiaohongshuService) ListFeeds(ctx context.Context) (*FeedsListResponse,
284285
// 获取 Feeds 列表
285286
feeds, err := action.GetFeedsList(ctx)
286287
if err != nil {
288+
logrus.Errorf("获取 Feeds 列表失败: %v", err)
287289
return nil, err
288290
}
289291

xiaohongshu/feed_detail.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"time"
88

99
"github.com/go-rod/rod"
10+
"github.com/sirupsen/logrus"
11+
"github.com/xpzouying/xiaohongshu-mcp/errors"
1012
)
1113

1214
// FeedDetailAction 表示 Feed 详情页动作
@@ -26,39 +28,37 @@ func (f *FeedDetailAction) GetFeedDetail(ctx context.Context, feedID, xsecToken
2628
// 构建详情页 URL
2729
url := makeFeedDetailURL(feedID, xsecToken)
2830

31+
logrus.Infof("打开 feed 详情页: %s", url)
32+
2933
// 导航到详情页
3034
page.MustNavigate(url)
3135
page.MustWaitDOMStable()
3236
time.Sleep(1 * time.Second)
3337

34-
// 获取 window.__INITIAL_STATE__ 并转换为 JSON 字符串
3538
result := page.MustEval(`() => {
36-
if (window.__INITIAL_STATE__) {
37-
return JSON.stringify(window.__INITIAL_STATE__);
39+
if (window.__INITIAL_STATE__ &&
40+
window.__INITIAL_STATE__.note &&
41+
window.__INITIAL_STATE__.note.noteDetailMap) {
42+
const noteDetailMap = window.__INITIAL_STATE__.note.noteDetailMap;
43+
return JSON.stringify(noteDetailMap);
3844
}
3945
return "";
4046
}`).String()
4147

4248
if result == "" {
43-
return nil, fmt.Errorf("__INITIAL_STATE__ not found")
49+
return nil, errors.ErrNoFeedDetail
4450
}
4551

46-
// 定义响应结构并直接反序列化
47-
var initialState struct {
48-
Note struct {
49-
NoteDetailMap map[string]struct {
50-
Note FeedDetail `json:"note"`
51-
Comments CommentList `json:"comments"`
52-
} `json:"noteDetailMap"`
53-
} `json:"note"`
52+
var noteDetailMap map[string]struct {
53+
Note FeedDetail `json:"note"`
54+
Comments CommentList `json:"comments"`
5455
}
5556

56-
if err := json.Unmarshal([]byte(result), &initialState); err != nil {
57-
return nil, fmt.Errorf("failed to unmarshal __INITIAL_STATE__: %w", err)
57+
if err := json.Unmarshal([]byte(result), &noteDetailMap); err != nil {
58+
return nil, fmt.Errorf("failed to unmarshal noteDetailMap: %w", err)
5859
}
5960

60-
// 从 noteDetailMap 中获取对应 feedID 的数据
61-
noteDetail, exists := initialState.Note.NoteDetailMap[feedID]
61+
noteDetail, exists := noteDetailMap[feedID]
6262
if !exists {
6363
return nil, fmt.Errorf("feed %s not found in noteDetailMap", feedID)
6464
}

xiaohongshu/feeds.go

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,13 @@ import (
77
"time"
88

99
"github.com/go-rod/rod"
10+
"github.com/xpzouying/xiaohongshu-mcp/errors"
1011
)
1112

1213
type FeedsListAction struct {
1314
page *rod.Page
1415
}
1516

16-
// FeedsResult 定义页面初始状态结构
17-
type FeedsResult struct {
18-
Feed FeedData `json:"feed"`
19-
}
20-
2117
func NewFeedsListAction(page *rod.Page) *FeedsListAction {
2218
pp := page.Timeout(60 * time.Second)
2319

@@ -33,24 +29,27 @@ func (f *FeedsListAction) GetFeedsList(ctx context.Context) ([]Feed, error) {
3329

3430
time.Sleep(1 * time.Second)
3531

36-
// 获取 window.__INITIAL_STATE__ 并转换为 JSON 字符串
3732
result := page.MustEval(`() => {
38-
if (window.__INITIAL_STATE__) {
39-
return JSON.stringify(window.__INITIAL_STATE__);
33+
if (window.__INITIAL_STATE__ &&
34+
window.__INITIAL_STATE__.feed &&
35+
window.__INITIAL_STATE__.feed.feeds) {
36+
const feeds = window.__INITIAL_STATE__.feed.feeds;
37+
const feedsData = feeds.value !== undefined ? feeds.value : feeds._value;
38+
if (feedsData) {
39+
return JSON.stringify(feedsData);
40+
}
4041
}
4142
return "";
4243
}`).String()
4344

4445
if result == "" {
45-
return nil, fmt.Errorf("__INITIAL_STATE__ not found")
46+
return nil, errors.ErrNoFeeds
4647
}
4748

48-
// 解析完整的 InitialState
49-
var state FeedsResult
50-
if err := json.Unmarshal([]byte(result), &state); err != nil {
51-
return nil, fmt.Errorf("failed to unmarshal __INITIAL_STATE__: %w", err)
49+
var feeds []Feed
50+
if err := json.Unmarshal([]byte(result), &feeds); err != nil {
51+
return nil, fmt.Errorf("failed to unmarshal feeds: %w", err)
5252
}
5353

54-
// 返回 feed.feeds._value
55-
return state.Feed.Feeds.Value, nil
54+
return feeds, nil
5655
}

xiaohongshu/like_favorite.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/go-rod/rod"
1010
"github.com/pkg/errors"
1111
"github.com/sirupsen/logrus"
12+
myerrors "github.com/xpzouying/xiaohongshu-mcp/errors"
1213
)
1314

1415
// ActionResult 通用动作响应(点赞/收藏等)
@@ -213,33 +214,33 @@ func (a *FavoriteAction) toggleFavorite(page *rod.Page, feedID string, targetCol
213214

214215
// getInteractState 从 __INITIAL_STATE__ 读取笔记的点赞/收藏状态
215216
func (a *interactAction) getInteractState(page *rod.Page, feedID string) (liked bool, collected bool, err error) {
217+
216218
result := page.MustEval(`() => {
217-
if (window.__INITIAL_STATE__) {
218-
return JSON.stringify(window.__INITIAL_STATE__);
219+
if (window.__INITIAL_STATE__ &&
220+
window.__INITIAL_STATE__.note &&
221+
window.__INITIAL_STATE__.note.noteDetailMap) {
222+
return JSON.stringify(window.__INITIAL_STATE__.note.noteDetailMap);
219223
}
220224
return "";
221225
}`).String()
222226
if result == "" {
223-
return false, false, fmt.Errorf("__INITIAL_STATE__ not found")
227+
return false, false, myerrors.ErrNoFeedDetail
224228
}
225229

226-
var state struct {
230+
// 直接解析为 noteDetailMap
231+
var noteDetailMap map[string]struct {
227232
Note struct {
228-
NoteDetailMap map[string]struct {
229-
Note struct {
230-
InteractInfo struct {
231-
Liked bool `json:"liked"`
232-
Collected bool `json:"collected"`
233-
} `json:"interactInfo"`
234-
} `json:"note"`
235-
} `json:"noteDetailMap"`
233+
InteractInfo struct {
234+
Liked bool `json:"liked"`
235+
Collected bool `json:"collected"`
236+
} `json:"interactInfo"`
236237
} `json:"note"`
237238
}
238-
if err := json.Unmarshal([]byte(result), &state); err != nil {
239-
return false, false, errors.Wrap(err, "unmarshal __INITIAL_STATE__ failed")
239+
if err := json.Unmarshal([]byte(result), &noteDetailMap); err != nil {
240+
return false, false, errors.Wrap(err, "unmarshal noteDetailMap failed")
240241
}
241242

242-
detail, ok := state.Note.NoteDetailMap[feedID]
243+
detail, ok := noteDetailMap[feedID]
243244
if !ok {
244245
return false, false, fmt.Errorf("feed %s not in noteDetailMap", feedID)
245246
}

xiaohongshu/search.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
"github.com/go-rod/rod"
11+
"github.com/xpzouying/xiaohongshu-mcp/errors"
1112
)
1213

1314
type SearchResult struct {
@@ -190,24 +191,29 @@ func (s *SearchAction) Search(ctx context.Context, keyword string, filters ...Fi
190191
page.MustWait(`() => window.__INITIAL_STATE__ !== undefined`)
191192
}
192193

193-
// 获取 window.__INITIAL_STATE__ 并转换为 JSON 字符串
194194
result := page.MustEval(`() => {
195-
if (window.__INITIAL_STATE__) {
196-
return JSON.stringify(window.__INITIAL_STATE__);
195+
if (window.__INITIAL_STATE__ &&
196+
window.__INITIAL_STATE__.search &&
197+
window.__INITIAL_STATE__.search.feeds) {
198+
const feeds = window.__INITIAL_STATE__.search.feeds;
199+
const feedsData = feeds.value !== undefined ? feeds.value : feeds._value;
200+
if (feedsData) {
201+
return JSON.stringify(feedsData);
197202
}
198-
return "";
199-
}`).String()
203+
}
204+
return "";
205+
}`).String()
200206

201207
if result == "" {
202-
return nil, fmt.Errorf("__INITIAL_STATE__ not found")
208+
return nil, errors.ErrNoFeeds
203209
}
204210

205-
var searchResult SearchResult
206-
if err := json.Unmarshal([]byte(result), &searchResult); err != nil {
207-
return nil, fmt.Errorf("failed to unmarshal __INITIAL_STATE__: %w", err)
211+
var feeds []Feed
212+
if err := json.Unmarshal([]byte(result), &feeds); err != nil {
213+
return nil, fmt.Errorf("failed to unmarshal feeds: %w", err)
208214
}
209215

210-
return searchResult.Search.Feeds.Value, nil
216+
return feeds, nil
211217
}
212218

213219
func makeSearchURL(keyword string) string {

xiaohongshu/user_profile.go

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,42 +33,71 @@ func (u *UserProfileAction) UserProfile(ctx context.Context, userID, xsecToken s
3333
func (u *UserProfileAction) extractUserProfileData(page *rod.Page) (*UserProfileResponse, error) {
3434
page.MustWait(`() => window.__INITIAL_STATE__ !== undefined`)
3535

36-
// 获取 window.__INITIAL_STATE__ 并转换为 JSON 字符串
37-
result := page.MustEval(`() => {
38-
if (window.__INITIAL_STATE__) {
39-
return JSON.stringify(window.__INITIAL_STATE__);
36+
userDataResult := page.MustEval(`() => {
37+
if (window.__INITIAL_STATE__ &&
38+
window.__INITIAL_STATE__.user &&
39+
window.__INITIAL_STATE__.user.userPageData) {
40+
const userPageData = window.__INITIAL_STATE__.user.userPageData;
41+
const data = userPageData.value !== undefined ? userPageData.value : userPageData._value;
42+
if (data) {
43+
return JSON.stringify(data);
4044
}
41-
return "";
42-
}`).String()
45+
}
46+
return "";
47+
}`).String()
4348

44-
if result == "" {
45-
return nil, fmt.Errorf("__INITIAL_STATE__ not found")
49+
if userDataResult == "" {
50+
return nil, fmt.Errorf("user.userPageData.value not found in __INITIAL_STATE__")
4651
}
47-
// 定义响应结构并直接反序列化
48-
var initialState = struct {
49-
User struct {
50-
UserPageData UserPageData `json:"userPageData"`
51-
Notes struct {
52-
Feeds [][]Feed `json:"_rawValue"` // 帖子为双重数组
53-
} `json:"notes"`
54-
} `json:"user"`
55-
}{}
56-
if err := json.Unmarshal([]byte(result), &initialState); err != nil {
57-
return nil, fmt.Errorf("failed to unmarshal __INITIAL_STATE__: %w", err)
52+
53+
// 2. 获取用户帖子:window.__INITIAL_STATE__.user.notes.value
54+
notesResult := page.MustEval(`() => {
55+
if (window.__INITIAL_STATE__ &&
56+
window.__INITIAL_STATE__.user &&
57+
window.__INITIAL_STATE__.user.notes) {
58+
const notes = window.__INITIAL_STATE__.user.notes;
59+
// 优先使用 value(getter),如果不存在则使用 _value(内部字段)
60+
const data = notes.value !== undefined ? notes.value : notes._value;
61+
if (data) {
62+
return JSON.stringify(data);
63+
}
64+
}
65+
return "";
66+
}`).String()
67+
68+
if notesResult == "" {
69+
return nil, fmt.Errorf("user.notes.value not found in __INITIAL_STATE__")
70+
}
71+
72+
// 解析用户信息
73+
var userPageData struct {
74+
Interactions []UserInteractions `json:"interactions"`
75+
BasicInfo UserBasicInfo `json:"basicInfo"`
5876
}
77+
if err := json.Unmarshal([]byte(userDataResult), &userPageData); err != nil {
78+
return nil, fmt.Errorf("failed to unmarshal userPageData: %w", err)
79+
}
80+
81+
// 解析帖子数据(帖子为双重数组)
82+
var notesFeeds [][]Feed
83+
if err := json.Unmarshal([]byte(notesResult), &notesFeeds); err != nil {
84+
return nil, fmt.Errorf("failed to unmarshal notes: %w", err)
85+
}
86+
87+
// 组装响应
5988
response := &UserProfileResponse{
60-
UserBasicInfo: initialState.User.UserPageData.RawValue.BasicInfo,
61-
Interactions: initialState.User.UserPageData.RawValue.Interactions,
89+
UserBasicInfo: userPageData.BasicInfo,
90+
Interactions: userPageData.Interactions,
6291
}
63-
// 添加用户贴子
64-
for _, feeds := range initialState.User.Notes.Feeds {
92+
93+
// 添加用户帖子(展平双重数组)
94+
for _, feeds := range notesFeeds {
6595
if len(feeds) != 0 {
6696
response.Feeds = append(response.Feeds, feeds...)
6797
}
6898
}
6999

70100
return response, nil
71-
72101
}
73102

74103
func makeUserProfileURL(userID, xsecToken string) string {

0 commit comments

Comments
 (0)