Files
xiaohongshu-mcp/xiaohongshu/like_favorite.go
haikow 66aa36b48c feat: add like and favorite functionality for feeds (#207)
* feat: add like and favorite functionality for feeds

- Implemented handleLikeFeed and handleFavoriteFeed methods in mcp_handlers.go to manage liking and favoriting feeds.
- Added LikeFavoriteArgs struct in mcp_server.go for handling parameters.
- Registered new MCP tools for liking and favoriting feeds in registerTools function.
- Introduced LikeFeed and FavoriteFeed methods in XiaohongshuService to interact with the respective actions.
- Created LikeFavoriteAction in a new file to encapsulate the logic for liking and favoriting feeds on the Xiaohongshu platform.

* "Fix-build-errors"

* refactor: streamline like and favorite actions in LikeFavoriteAction

- Introduced a generic method `performInteractAction` to handle both liking and favoriting feeds, reducing code duplication.
- Updated logging to reflect the action type being performed (like or favorite).
- Enhanced state verification after interaction to ensure accurate feedback on success or failure.
- Removed the `clickLastMatch` function, simplifying the interaction logic.

* "Add-unlike-and-unfavorite-functionality"

* "Refactor-performInteractAction-function"

* "Refactor-split-LikeFavoriteAction-into-separate-actions"

---------

Co-authored-by: chekayo <9827969+chekayo@user.noreply.gitee.com>
2025-10-08 11:40:45 +08:00

248 lines
7.0 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 xiaohongshu
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/go-rod/rod"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// ActionResult 通用动作响应(点赞/收藏等)
type ActionResult struct {
FeedID string `json:"feed_id"`
Success bool `json:"success"`
Message string `json:"message"`
}
// 选择器常量
const (
SelectorLikeButton = ".interact-container .left .like-lottie"
SelectorCollectButton = ".interact-container .left .reds-icon.collect-icon"
)
// interactActionType 交互动作类型
type interactActionType string
const (
actionLike interactActionType = "点赞"
actionFavorite interactActionType = "收藏"
actionUnlike interactActionType = "取消点赞"
actionUnfavorite interactActionType = "取消收藏"
)
type interactAction struct {
page *rod.Page
}
func newInteractAction(page *rod.Page) *interactAction {
return &interactAction{page: page}
}
func (a *interactAction) preparePage(ctx context.Context, actionType interactActionType, feedID, xsecToken string) *rod.Page {
page := a.page.Context(ctx).Timeout(60 * time.Second)
url := makeFeedDetailURL(feedID, xsecToken)
logrus.Infof("Opening feed detail page for %s: %s", actionType, url)
page.MustNavigate(url)
page.MustWaitDOMStable()
time.Sleep(1 * time.Second)
return page
}
func (a *interactAction) performClick(page *rod.Page, selector string) {
element := page.MustElement(selector)
element.MustClick()
}
// LikeAction 负责处理点赞相关交互
type LikeAction struct {
*interactAction
}
func NewLikeAction(page *rod.Page) *LikeAction {
return &LikeAction{interactAction: newInteractAction(page)}
}
// Like 点赞指定笔记,如果已点赞则直接返回
func (a *LikeAction) Like(ctx context.Context, feedID, xsecToken string) error {
return a.perform(ctx, feedID, xsecToken, true)
}
// Unlike 取消点赞指定笔记,如果未点赞则直接返回
func (a *LikeAction) Unlike(ctx context.Context, feedID, xsecToken string) error {
return a.perform(ctx, feedID, xsecToken, false)
}
func (a *LikeAction) perform(ctx context.Context, feedID, xsecToken string, targetLiked bool) error {
actionType := actionLike
if !targetLiked {
actionType = actionUnlike
}
page := a.preparePage(ctx, actionType, feedID, xsecToken)
liked, _, err := a.getInteractState(page, feedID)
if err != nil {
logrus.Warnf("failed to read interact state: %v (continue to try clicking)", err)
return a.toggleLike(page, feedID, targetLiked, actionType)
}
if targetLiked && liked {
logrus.Infof("feed %s already liked, skip clicking", feedID)
return nil
}
if !targetLiked && !liked {
logrus.Infof("feed %s not liked yet, skip clicking", feedID)
return nil
}
return a.toggleLike(page, feedID, targetLiked, actionType)
}
func (a *LikeAction) toggleLike(page *rod.Page, feedID string, targetLiked bool, actionType interactActionType) error {
a.performClick(page, SelectorLikeButton)
time.Sleep(3 * time.Second)
liked, _, err := a.getInteractState(page, feedID)
if err != nil {
logrus.Warnf("验证%s状态失败: %v", actionType, err)
return nil
}
if liked == targetLiked {
logrus.Infof("feed %s %s成功", feedID, actionType)
return nil
}
logrus.Warnf("feed %s %s可能未成功状态未变化尝试再次点击", feedID, actionType)
a.performClick(page, SelectorLikeButton)
time.Sleep(2 * time.Second)
liked, _, err = a.getInteractState(page, feedID)
if err != nil {
logrus.Warnf("第二次验证%s状态失败: %v", actionType, err)
return nil
}
if liked == targetLiked {
logrus.Infof("feed %s 第二次点击%s成功", feedID, actionType)
return nil
}
return nil
}
// FavoriteAction 负责处理收藏相关交互
type FavoriteAction struct {
*interactAction
}
func NewFavoriteAction(page *rod.Page) *FavoriteAction {
return &FavoriteAction{interactAction: newInteractAction(page)}
}
// Favorite 收藏指定笔记,如果已收藏则直接返回
func (a *FavoriteAction) Favorite(ctx context.Context, feedID, xsecToken string) error {
return a.perform(ctx, feedID, xsecToken, true)
}
// Unfavorite 取消收藏指定笔记,如果未收藏则直接返回
func (a *FavoriteAction) Unfavorite(ctx context.Context, feedID, xsecToken string) error {
return a.perform(ctx, feedID, xsecToken, false)
}
func (a *FavoriteAction) perform(ctx context.Context, feedID, xsecToken string, targetCollected bool) error {
actionType := actionFavorite
if !targetCollected {
actionType = actionUnfavorite
}
page := a.preparePage(ctx, actionType, feedID, xsecToken)
_, collected, err := a.getInteractState(page, feedID)
if err != nil {
logrus.Warnf("failed to read interact state: %v (continue to try clicking)", err)
return a.toggleFavorite(page, feedID, targetCollected, actionType)
}
if targetCollected && collected {
logrus.Infof("feed %s already favorited, skip clicking", feedID)
return nil
}
if !targetCollected && !collected {
logrus.Infof("feed %s not favorited yet, skip clicking", feedID)
return nil
}
return a.toggleFavorite(page, feedID, targetCollected, actionType)
}
func (a *FavoriteAction) toggleFavorite(page *rod.Page, feedID string, targetCollected bool, actionType interactActionType) error {
a.performClick(page, SelectorCollectButton)
time.Sleep(3 * time.Second)
_, collected, err := a.getInteractState(page, feedID)
if err != nil {
logrus.Warnf("验证%s状态失败: %v", actionType, err)
return nil
}
if collected == targetCollected {
logrus.Infof("feed %s %s成功", feedID, actionType)
return nil
}
logrus.Warnf("feed %s %s可能未成功状态未变化尝试再次点击", feedID, actionType)
a.performClick(page, SelectorCollectButton)
time.Sleep(2 * time.Second)
_, collected, err = a.getInteractState(page, feedID)
if err != nil {
logrus.Warnf("第二次验证%s状态失败: %v", actionType, err)
return nil
}
if collected == targetCollected {
logrus.Infof("feed %s 第二次点击%s成功", feedID, actionType)
return nil
}
return nil
}
// getInteractState 从 __INITIAL_STATE__ 读取笔记的点赞/收藏状态
func (a *interactAction) getInteractState(page *rod.Page, feedID string) (liked bool, collected bool, err error) {
result := page.MustEval(`() => {
if (window.__INITIAL_STATE__) {
return JSON.stringify(window.__INITIAL_STATE__);
}
return "";
}`).String()
if result == "" {
return false, false, fmt.Errorf("__INITIAL_STATE__ not found")
}
var state struct {
Note struct {
NoteDetailMap map[string]struct {
Note struct {
InteractInfo struct {
Liked bool `json:"liked"`
Collected bool `json:"collected"`
} `json:"interactInfo"`
} `json:"note"`
} `json:"noteDetailMap"`
} `json:"note"`
}
if err := json.Unmarshal([]byte(result), &state); err != nil {
return false, false, errors.Wrap(err, "unmarshal __INITIAL_STATE__ failed")
}
detail, ok := state.Note.NoteDetailMap[feedID]
if !ok {
return false, false, fmt.Errorf("feed %s not in noteDetailMap", feedID)
}
return detail.Note.InteractInfo.Liked, detail.Note.InteractInfo.Collected, nil
}