* 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>
195 lines
4.5 KiB
Go
195 lines
4.5 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
|
||
"github.com/xpzouying/xiaohongshu-mcp/browser"
|
||
"github.com/xpzouying/xiaohongshu-mcp/configs"
|
||
"github.com/xpzouying/xiaohongshu-mcp/pkg/downloader"
|
||
"github.com/xpzouying/xiaohongshu-mcp/xiaohongshu"
|
||
)
|
||
|
||
// XiaohongshuService 小红书业务服务
|
||
type XiaohongshuService struct{}
|
||
|
||
// NewXiaohongshuService 创建小红书服务实例
|
||
func NewXiaohongshuService() *XiaohongshuService {
|
||
return &XiaohongshuService{}
|
||
}
|
||
|
||
// PublishRequest 发布请求
|
||
type PublishRequest struct {
|
||
Title string `json:"title" binding:"required"`
|
||
Content string `json:"content" binding:"required"`
|
||
Images []string `json:"images" binding:"required,min=1"`
|
||
}
|
||
|
||
// LoginStatusResponse 登录状态响应
|
||
type LoginStatusResponse struct {
|
||
IsLoggedIn bool `json:"is_logged_in"`
|
||
Username string `json:"username,omitempty"`
|
||
}
|
||
|
||
// PublishResponse 发布响应
|
||
type PublishResponse struct {
|
||
Title string `json:"title"`
|
||
Content string `json:"content"`
|
||
Images int `json:"images"`
|
||
Status string `json:"status"`
|
||
PostID string `json:"post_id,omitempty"`
|
||
}
|
||
|
||
// FeedsListResponse Feeds列表响应
|
||
type FeedsListResponse struct {
|
||
Feeds []xiaohongshu.Feed `json:"feeds"`
|
||
Count int `json:"count"`
|
||
}
|
||
|
||
// CheckLoginStatus 检查登录状态
|
||
func (s *XiaohongshuService) CheckLoginStatus(ctx context.Context) (*LoginStatusResponse, error) {
|
||
b := browser.NewBrowser(configs.IsHeadless())
|
||
defer b.Close()
|
||
|
||
page := b.NewPage()
|
||
defer page.Close()
|
||
|
||
loginAction := xiaohongshu.NewLogin(page)
|
||
|
||
isLoggedIn, err := loginAction.CheckLoginStatus(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
response := &LoginStatusResponse{
|
||
IsLoggedIn: isLoggedIn,
|
||
Username: configs.Username,
|
||
}
|
||
|
||
return response, nil
|
||
}
|
||
|
||
// PublishContent 发布内容
|
||
func (s *XiaohongshuService) PublishContent(ctx context.Context, req *PublishRequest) (*PublishResponse, error) {
|
||
// 处理图片:下载URL图片或使用本地路径
|
||
imagePaths, err := s.processImages(req.Images)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 构建发布内容
|
||
content := xiaohongshu.PublishImageContent{
|
||
Title: req.Title,
|
||
Content: req.Content,
|
||
ImagePaths: imagePaths,
|
||
}
|
||
|
||
// 执行发布
|
||
if err := s.publishContent(ctx, content); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
response := &PublishResponse{
|
||
Title: req.Title,
|
||
Content: req.Content,
|
||
Images: len(imagePaths),
|
||
Status: "发布完成",
|
||
}
|
||
|
||
return response, nil
|
||
}
|
||
|
||
// processImages 处理图片列表,支持URL下载和本地路径
|
||
func (s *XiaohongshuService) processImages(images []string) ([]string, error) {
|
||
processor := downloader.NewImageProcessor()
|
||
return processor.ProcessImages(images)
|
||
}
|
||
|
||
// publishContent 执行内容发布
|
||
func (s *XiaohongshuService) publishContent(ctx context.Context, content xiaohongshu.PublishImageContent) error {
|
||
b := browser.NewBrowser(configs.IsHeadless())
|
||
defer b.Close()
|
||
|
||
page := b.NewPage()
|
||
defer page.Close()
|
||
|
||
action, err := xiaohongshu.NewPublishImageAction(page)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 执行发布
|
||
return action.Publish(ctx, content)
|
||
}
|
||
|
||
// ListFeeds 获取Feeds列表
|
||
func (s *XiaohongshuService) ListFeeds(ctx context.Context) (*FeedsListResponse, error) {
|
||
b := browser.NewBrowser(configs.IsHeadless())
|
||
defer b.Close()
|
||
|
||
page := b.NewPage()
|
||
defer page.Close()
|
||
|
||
// 创建 Feeds 列表 action
|
||
action := xiaohongshu.NewFeedsListAction(page)
|
||
|
||
// 获取 Feeds 列表
|
||
feeds, err := action.GetFeedsList(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
response := &FeedsListResponse{
|
||
Feeds: feeds,
|
||
Count: len(feeds),
|
||
}
|
||
|
||
return response, nil
|
||
}
|
||
|
||
func (s *XiaohongshuService) SearchFeeds(ctx context.Context, keyword string) (*FeedsListResponse, error) {
|
||
b := browser.NewBrowser(configs.IsHeadless())
|
||
defer b.Close()
|
||
|
||
page := b.NewPage()
|
||
defer page.Close()
|
||
|
||
action := xiaohongshu.NewSearchAction(page)
|
||
|
||
feeds, err := action.Search(ctx, keyword)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
response := &FeedsListResponse{
|
||
Feeds: feeds,
|
||
Count: len(feeds),
|
||
}
|
||
|
||
return response, nil
|
||
}
|
||
|
||
// GetFeedDetail 获取Feed详情
|
||
func (s *XiaohongshuService) GetFeedDetail(ctx context.Context, feedID, xsecToken string) (*FeedDetailResponse, error) {
|
||
b := browser.NewBrowser(configs.IsHeadless())
|
||
defer b.Close()
|
||
|
||
page := b.NewPage()
|
||
defer page.Close()
|
||
|
||
// 创建 Feed 详情 action
|
||
action := xiaohongshu.NewFeedDetailAction(page)
|
||
|
||
// 获取 Feed 详情
|
||
result, err := action.GetFeedDetail(ctx, feedID, xsecToken)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
response := &FeedDetailResponse{
|
||
FeedID: feedID,
|
||
Data: result,
|
||
}
|
||
|
||
return response, nil
|
||
}
|