feat: enhance feed detail functionality with MCP interface improvements (#45)
* feat: add feed detail page functionality with gin and MCP interfaces Add comprehensive Feed detail page support: - Create new FeedDetailAction in xiaohongshu/feed_detail.go - Add HTTP API endpoint POST /api/v1/feeds/detail - Add MCP tool 'get_feed_detail' for MCP protocol support - Support feed_id and xsec_token parameters (both required) - Raw __INITIAL_STATE__ JSON data saved to feed_detail.json - Return structured data for both HTTP and MCP interfaces 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: enhance feed detail functionality with MCP interface improvements - Add comprehensive feed detail page support with proper data extraction - Create dedicated feed_detail.go file for FeedDetailAction - Optimize Go struct definitions based on actual JSON data analysis - Remove unnecessary fields from FeedDetail, DetailImageInfo, CommentList, and Comment structs - Update MCP interface description to reflect comment retrieval capability - Support both HTTP REST API and MCP protocol interfaces - Implement proper Vue 3 reactive data extraction from window.__INITIAL_STATE__ - Include feed content, user info, interaction data, and comment lists 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: restore JSON file writing for testing and improve code structure - Restore feed_detail.json file writing for testing purposes - Improve error handling by separating marshal and unmarshal steps - Keep the original data extraction logic for complex Vue reactive data structure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: simplify JSON unmarshaling using struct instead of map[string]any - Replace complex map[string]any extraction with direct struct unmarshaling - Define inline struct matching the actual JSON response structure - Remove unnecessary extractFeedDetailData and extractNestedValue methods - Significantly reduce code complexity and improve readability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: improve MCP interface descriptions for better usability - Enhance get_feed_detail parameter descriptions with clear source information - Clarify publish_content images parameter supports both local paths and URLs - Improve search_feeds description to specify supported search types - Keep descriptions concise and practical without over-complication 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: revert search_feeds keyword description to keep it simple - Remove unnecessary details from keyword description - Keep interface descriptions concise and clear 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
77
xiaohongshu/feed_detail.go
Normal file
77
xiaohongshu/feed_detail.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package xiaohongshu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
)
|
||||
|
||||
// FeedDetailAction 表示 Feed 详情页动作
|
||||
type FeedDetailAction struct {
|
||||
page *rod.Page
|
||||
}
|
||||
|
||||
// NewFeedDetailAction 创建 Feed 详情页动作
|
||||
func NewFeedDetailAction(page *rod.Page) *FeedDetailAction {
|
||||
return &FeedDetailAction{page: page}
|
||||
}
|
||||
|
||||
// GetFeedDetail 获取 Feed 详情页数据
|
||||
func (f *FeedDetailAction) GetFeedDetail(ctx context.Context, feedID, xsecToken string) (*FeedDetailResponse, error) {
|
||||
page := f.page.Context(ctx).Timeout(60 * time.Second)
|
||||
|
||||
// 构建详情页 URL
|
||||
url := fmt.Sprintf("https://www.xiaohongshu.com/explore/%s?xsec_token=%s&xsec_source=pc_feed", feedID, xsecToken)
|
||||
|
||||
// 导航到详情页
|
||||
page.MustNavigate(url)
|
||||
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")
|
||||
}
|
||||
|
||||
// 将原始结果保存到 feed_detail.json 文件用于测试
|
||||
err := os.WriteFile("feed_detail.json", []byte(result), 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to write feed_detail.json: %w", err)
|
||||
}
|
||||
|
||||
// 定义响应结构并直接反序列化
|
||||
var initialState struct {
|
||||
Note struct {
|
||||
NoteDetailMap map[string]struct {
|
||||
Note FeedDetail `json:"note"`
|
||||
Comments CommentList `json:"comments"`
|
||||
} `json:"noteDetailMap"`
|
||||
} `json:"note"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(result), &initialState); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal __INITIAL_STATE__: %w", err)
|
||||
}
|
||||
|
||||
// 从 noteDetailMap 中获取对应 feedID 的数据
|
||||
noteDetail, exists := initialState.Note.NoteDetailMap[feedID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("feed %s not found in noteDetailMap", feedID)
|
||||
}
|
||||
|
||||
return &FeedDetailResponse{
|
||||
Note: noteDetail.Note,
|
||||
Comments: noteDetail.Comments,
|
||||
}, nil
|
||||
}
|
||||
@@ -83,3 +83,56 @@ type Video struct {
|
||||
type VideoCapability struct {
|
||||
Duration int `json:"duration"` // 视频时长,单位秒
|
||||
}
|
||||
|
||||
// ================ Feed 详情页相关结构体 ================
|
||||
|
||||
// FeedDetailResponse 表示 Feed 详情页完整响应
|
||||
type FeedDetailResponse struct {
|
||||
Note FeedDetail `json:"note"`
|
||||
Comments CommentList `json:"comments"`
|
||||
}
|
||||
|
||||
// FeedDetail 表示详情页的笔记内容
|
||||
type FeedDetail struct {
|
||||
NoteID string `json:"noteId"`
|
||||
XsecToken string `json:"xsecToken"`
|
||||
Title string `json:"title"`
|
||||
Desc string `json:"desc"`
|
||||
Type string `json:"type"`
|
||||
Time int64 `json:"time"`
|
||||
IPLocation string `json:"ipLocation"`
|
||||
User User `json:"user"`
|
||||
InteractInfo InteractInfo `json:"interactInfo"`
|
||||
ImageList []DetailImageInfo `json:"imageList"`
|
||||
}
|
||||
|
||||
// DetailImageInfo 表示详情页的图片信息
|
||||
type DetailImageInfo struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
URLDefault string `json:"urlDefault"`
|
||||
URLPre string `json:"urlPre"`
|
||||
LivePhoto bool `json:"livePhoto,omitempty"`
|
||||
}
|
||||
|
||||
// CommentList 表示评论列表
|
||||
type CommentList struct {
|
||||
List []Comment `json:"list"`
|
||||
Cursor string `json:"cursor"`
|
||||
HasMore bool `json:"hasMore"`
|
||||
}
|
||||
|
||||
// Comment 表示单条评论
|
||||
type Comment struct {
|
||||
ID string `json:"id"`
|
||||
NoteID string `json:"noteId"`
|
||||
Content string `json:"content"`
|
||||
LikeCount string `json:"likeCount"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
IPLocation string `json:"ipLocation"`
|
||||
Liked bool `json:"liked"`
|
||||
UserInfo User `json:"userInfo"`
|
||||
SubCommentCount string `json:"subCommentCount"`
|
||||
SubComments []Comment `json:"subComments"`
|
||||
ShowTags []string `json:"showTags"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user