Files
xiaohongshu-mcp/service.go
zy 27575689a6 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>
2025-09-09 00:55:24 +08:00

195 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}