fix:fix
This commit is contained in:
3
go.mod
3
go.mod
@@ -3,7 +3,7 @@ module github.com/xpzouying/xiaohongshu-mcp
|
|||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/avast/retry-go/v4 v4.6.0
|
github.com/avast/retry-go/v4 v4.7.0
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/go-rod/rod v0.116.2
|
github.com/go-rod/rod v0.116.2
|
||||||
github.com/h2non/filetype v1.1.3
|
github.com/h2non/filetype v1.1.3
|
||||||
@@ -16,7 +16,6 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/avast/retry-go/v4 v4.7.0 // indirect
|
|
||||||
github.com/bytedance/sonic v1.11.6 // indirect
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -79,8 +79,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
|||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/xpzouying/xiaohongshu-mcp/cookies"
|
"github.com/xpzouying/xiaohongshu-mcp/cookies"
|
||||||
"github.com/xpzouying/xiaohongshu-mcp/xiaohongshu"
|
"github.com/xpzouying/xiaohongshu-mcp/xiaohongshu"
|
||||||
@@ -401,67 +397,7 @@ func (s *AppServer) handleGetFeedDetail(ctx context.Context, args map[string]any
|
|||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("MCP: 获取Feed详情 - Feed ID: %s, loadAllComments=%v, config=%+v", feedID, loadAll, config)
|
logrus.Infof("MCP: 获取Feed详情 - Feed ID: %s, loadAllComments=%v, config=%+v", feedID, loadAll, config)
|
||||||
loadAll := false
|
|
||||||
if raw, ok := args["load_all_comments"]; ok {
|
|
||||||
switch v := raw.(type) {
|
|
||||||
case bool:
|
|
||||||
loadAll = v
|
|
||||||
case string:
|
|
||||||
if parsed, err := strconv.ParseBool(v); err == nil {
|
|
||||||
loadAll = parsed
|
|
||||||
}
|
|
||||||
case float64:
|
|
||||||
loadAll = v != 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析评论配置参数,如果未提供则使用默认值
|
|
||||||
config := xiaohongshu.DefaultCommentLoadConfig()
|
|
||||||
|
|
||||||
if raw, ok := args["click_more_replies"]; ok {
|
|
||||||
switch v := raw.(type) {
|
|
||||||
case bool:
|
|
||||||
config.ClickMoreReplies = v
|
|
||||||
case string:
|
|
||||||
if parsed, err := strconv.ParseBool(v); err == nil {
|
|
||||||
config.ClickMoreReplies = parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, ok := args["max_replies_threshold"]; ok {
|
|
||||||
switch v := raw.(type) {
|
|
||||||
case float64:
|
|
||||||
config.MaxRepliesThreshold = int(v)
|
|
||||||
case string:
|
|
||||||
if parsed, err := strconv.Atoi(v); err == nil {
|
|
||||||
config.MaxRepliesThreshold = parsed
|
|
||||||
}
|
|
||||||
case int:
|
|
||||||
config.MaxRepliesThreshold = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, ok := args["max_comment_items"]; ok {
|
|
||||||
switch v := raw.(type) {
|
|
||||||
case float64:
|
|
||||||
config.MaxCommentItems = int(v)
|
|
||||||
case string:
|
|
||||||
if parsed, err := strconv.Atoi(v); err == nil {
|
|
||||||
config.MaxCommentItems = parsed
|
|
||||||
}
|
|
||||||
case int:
|
|
||||||
config.MaxCommentItems = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if raw, ok := args["scroll_speed"].(string); ok && raw != "" {
|
|
||||||
config.ScrollSpeed = raw
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Infof("MCP: 获取Feed详情 - Feed ID: %s, loadAllComments=%v, config=%+v", feedID, loadAll, config)
|
|
||||||
|
|
||||||
result, err := s.xiaohongshuService.GetFeedDetailWithConfig(ctx, feedID, xsecToken, loadAll, config)
|
|
||||||
result, err := s.xiaohongshuService.GetFeedDetailWithConfig(ctx, feedID, xsecToken, loadAll, config)
|
result, err := s.xiaohongshuService.GetFeedDetailWithConfig(ctx, feedID, xsecToken, loadAll, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &MCPToolResult{
|
return &MCPToolResult{
|
||||||
|
|||||||
@@ -237,13 +237,6 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
|
|||||||
"max_replies_threshold": args.MaxRepliesThreshold,
|
"max_replies_threshold": args.MaxRepliesThreshold,
|
||||||
"max_comment_items": args.MaxCommentItems,
|
"max_comment_items": args.MaxCommentItems,
|
||||||
"scroll_speed": args.ScrollSpeed,
|
"scroll_speed": args.ScrollSpeed,
|
||||||
"feed_id": args.FeedID,
|
|
||||||
"xsec_token": args.XsecToken,
|
|
||||||
"load_all_comments": args.LoadAllComments,
|
|
||||||
"click_more_replies": args.ClickMoreReplies,
|
|
||||||
"max_replies_threshold": args.MaxRepliesThreshold,
|
|
||||||
"max_comment_items": args.MaxCommentItems,
|
|
||||||
"scroll_speed": args.ScrollSpeed,
|
|
||||||
}
|
}
|
||||||
result := appServer.handleGetFeedDetail(ctx, argsMap)
|
result := appServer.handleGetFeedDetail(ctx, argsMap)
|
||||||
return convertToMCPResult(result), nil, nil
|
return convertToMCPResult(result), nil, nil
|
||||||
|
|||||||
14
types.go
14
types.go
@@ -46,26 +46,12 @@ type CommentLoadConfig struct {
|
|||||||
ScrollSpeed string `json:"scroll_speed,omitempty"`
|
ScrollSpeed string `json:"scroll_speed,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommentLoadConfig 评论加载配置
|
|
||||||
type CommentLoadConfig struct {
|
|
||||||
// 是否点击"更多回复"按钮
|
|
||||||
ClickMoreReplies bool `json:"click_more_replies,omitempty"`
|
|
||||||
// 回复数量阈值,超过这个数量的"更多"按钮将被跳过(0表示不跳过任何)
|
|
||||||
MaxRepliesThreshold int `json:"max_replies_threshold,omitempty"`
|
|
||||||
// 最大加载评论数(comment-item数量),0表示加载所有
|
|
||||||
MaxCommentItems int `json:"max_comment_items,omitempty"`
|
|
||||||
// 滚动速度等级: slow(慢速), normal(正常), fast(快速)
|
|
||||||
ScrollSpeed string `json:"scroll_speed,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// FeedDetailRequest Feed详情请求
|
// FeedDetailRequest Feed详情请求
|
||||||
type FeedDetailRequest struct {
|
type FeedDetailRequest struct {
|
||||||
FeedID string `json:"feed_id" binding:"required"`
|
FeedID string `json:"feed_id" binding:"required"`
|
||||||
XsecToken string `json:"xsec_token" binding:"required"`
|
XsecToken string `json:"xsec_token" binding:"required"`
|
||||||
LoadAllComments bool `json:"load_all_comments,omitempty"`
|
LoadAllComments bool `json:"load_all_comments,omitempty"`
|
||||||
CommentConfig *CommentLoadConfig `json:"comment_config,omitempty"`
|
CommentConfig *CommentLoadConfig `json:"comment_config,omitempty"`
|
||||||
LoadAllComments bool `json:"load_all_comments,omitempty"`
|
|
||||||
CommentConfig *CommentLoadConfig `json:"comment_config,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchFeedsRequest struct {
|
type SearchFeedsRequest struct {
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ func NewCommentFeedAction(page *rod.Page) *CommentFeedAction {
|
|||||||
|
|
||||||
// PostComment 发表评论到 Feed
|
// PostComment 发表评论到 Feed
|
||||||
func (f *CommentFeedAction) PostComment(ctx context.Context, feedID, xsecToken, content string) error {
|
func (f *CommentFeedAction) PostComment(ctx context.Context, feedID, xsecToken, content string) error {
|
||||||
page := f.page.Context(ctx).Timeout(60 * time.Second)
|
// 不使用 Context(ctx),避免继承外部 context 的超时
|
||||||
|
page := f.page.Timeout(60 * time.Second)
|
||||||
|
|
||||||
url := makeFeedDetailURL(feedID, xsecToken)
|
url := makeFeedDetailURL(feedID, xsecToken)
|
||||||
logrus.Infof("打开 feed 详情页: %s", url)
|
logrus.Infof("打开 feed 详情页: %s", url)
|
||||||
@@ -81,7 +82,8 @@ func (f *CommentFeedAction) PostComment(ctx context.Context, feedID, xsecToken,
|
|||||||
// ReplyToComment 回复指定评论
|
// ReplyToComment 回复指定评论
|
||||||
func (f *CommentFeedAction) ReplyToComment(ctx context.Context, feedID, xsecToken, commentID, userID, content string) error {
|
func (f *CommentFeedAction) ReplyToComment(ctx context.Context, feedID, xsecToken, commentID, userID, content string) error {
|
||||||
// 增加超时时间,因为需要滚动查找评论
|
// 增加超时时间,因为需要滚动查找评论
|
||||||
page := f.page.Context(ctx).Timeout(5 * time.Minute)
|
// 注意:不使用 Context(ctx),避免继承外部 context 的超时
|
||||||
|
page := f.page.Timeout(5 * time.Minute)
|
||||||
url := makeFeedDetailURL(feedID, xsecToken)
|
url := makeFeedDetailURL(feedID, xsecToken)
|
||||||
logrus.Infof("打开 feed 详情页进行回复: %s", url)
|
logrus.Infof("打开 feed 详情页进行回复: %s", url)
|
||||||
|
|
||||||
@@ -151,50 +153,122 @@ func (f *CommentFeedAction) ReplyToComment(ctx context.Context, feedID, xsecToke
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// findCommentElement 查找指定评论元素(Go 实现,减少 JS 代码)
|
// findCommentElement 查找指定评论元素(参考 feed_detail.go 的滚动逻辑)
|
||||||
func findCommentElement(page *rod.Page, commentID, userID string) (*rod.Element, error) {
|
func findCommentElement(page *rod.Page, commentID, userID string) (*rod.Element, error) {
|
||||||
logrus.Infof("开始查找评论 - commentID: %s, userID: %s", commentID, userID)
|
logrus.Infof("开始查找评论 - commentID: %s, userID: %s", commentID, userID)
|
||||||
|
|
||||||
const maxAttempts = 50
|
const maxAttempts = 100
|
||||||
const scrollInterval = 800 * time.Millisecond
|
const scrollInterval = 800 * time.Millisecond
|
||||||
|
|
||||||
// 先滚动到评论区
|
// 先滚动到评论区
|
||||||
page.MustEval(`() => {
|
scrollToCommentsArea(page)
|
||||||
const container = document.querySelector('.comments-container');
|
|
||||||
if (container) {
|
|
||||||
container.scrollIntoView({behavior: 'smooth', block: 'start'});
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
var lastCommentCount = 0
|
||||||
logrus.Debugf("查找尝试 %d/%d", attempt+1, maxAttempts)
|
stagnantChecks := 0
|
||||||
|
|
||||||
// 优先通过 commentID 查找
|
logrus.Infof("开始循环查找,最大尝试次数: %d", maxAttempts)
|
||||||
|
|
||||||
|
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||||
|
logrus.Infof("=== 查找尝试 %d/%d ===", attempt+1, maxAttempts)
|
||||||
|
|
||||||
|
// === 1. 检查是否到达底部 ===
|
||||||
|
if checkEndContainer(page) {
|
||||||
|
logrus.Info("已到达评论底部,未找到目标评论")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 2. 获取当前评论数量 ===
|
||||||
|
currentCount := getCommentCount(page)
|
||||||
|
logrus.Infof("当前评论数: %d", currentCount)
|
||||||
|
|
||||||
|
if currentCount != lastCommentCount {
|
||||||
|
logrus.Infof("✓ 评论数增加: %d -> %d", lastCommentCount, currentCount)
|
||||||
|
lastCommentCount = currentCount
|
||||||
|
stagnantChecks = 0
|
||||||
|
} else {
|
||||||
|
stagnantChecks++
|
||||||
|
if stagnantChecks%5 == 0 {
|
||||||
|
logrus.Infof("评论数停滞 %d 次", stagnantChecks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 3. 停滞检测 ===
|
||||||
|
if stagnantChecks >= 10 {
|
||||||
|
logrus.Info("评论数量停滞超过10次,可能已加载完所有评论")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 4. 先滚动到最后一个评论(触发懒加载)===
|
||||||
|
if currentCount > 0 {
|
||||||
|
logrus.Infof("滚动到最后一个评论(共 %d 条)", currentCount)
|
||||||
|
_, err := page.Eval(`() => {
|
||||||
|
const container = document.querySelector('.comments-container');
|
||||||
|
if (!container) return false;
|
||||||
|
|
||||||
|
// 查找最后一个评论
|
||||||
|
const comments = container.querySelectorAll('.parent-comment, .comment-item, .comment');
|
||||||
|
if (comments.length > 0) {
|
||||||
|
const lastComment = comments[comments.length - 1];
|
||||||
|
lastComment.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}`)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("滚动到最后一个评论失败: %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 5. 继续向下滚动 ===
|
||||||
|
logrus.Infof("继续向下滚动...")
|
||||||
|
_, err := page.Eval(`() => { window.scrollBy(0, window.innerHeight * 0.8); return true; }`)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("滚动失败: %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
// === 6. 滚动后立即查找(边滚动边查找)===
|
||||||
|
// 优先通过 commentID 查找(使用 Timeout 避免长时间等待)
|
||||||
if commentID != "" {
|
if commentID != "" {
|
||||||
selector := fmt.Sprintf("#comment-%s", commentID)
|
selector := fmt.Sprintf("#comment-%s", commentID)
|
||||||
if el, err := page.Element(selector); err == nil {
|
logrus.Infof("尝试通过 commentID 查找: %s", selector)
|
||||||
logrus.Infof("✓ 通过 commentID 找到评论: %s", commentID)
|
|
||||||
|
// 使用 Timeout 避免长时间等待
|
||||||
|
el, err := page.Timeout(2 * time.Second).Element(selector)
|
||||||
|
if err == nil && el != nil {
|
||||||
|
logrus.Infof("✓ 通过 commentID 找到评论: %s (尝试 %d 次)", commentID, attempt+1)
|
||||||
return el, nil
|
return el, nil
|
||||||
}
|
}
|
||||||
|
logrus.Infof("未找到 commentID (2秒超时)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通过 userID 查找
|
// 通过 userID 查找
|
||||||
if userID != "" {
|
if userID != "" {
|
||||||
elements, err := page.Elements(".comment-item, .comment")
|
logrus.Infof("尝试通过 userID 查找: %s", userID)
|
||||||
if err == nil {
|
|
||||||
for _, el := range elements {
|
// 使用 Timeout 避免长时间等待
|
||||||
userEl, err := el.Element(fmt.Sprintf(`[data-user-id="%s"]`, userID))
|
elements, err := page.Timeout(2 * time.Second).Elements(".comment-item, .comment, .parent-comment")
|
||||||
|
if err == nil && len(elements) > 0 {
|
||||||
|
logrus.Infof("找到 %d 个评论元素", len(elements))
|
||||||
|
for i, el := range elements {
|
||||||
|
// 快速检查,不等待
|
||||||
|
userEl, err := el.Timeout(500 * time.Millisecond).Element(fmt.Sprintf(`[data-user-id="%s"]`, userID))
|
||||||
if err == nil && userEl != nil {
|
if err == nil && userEl != nil {
|
||||||
logrus.Infof("✓ 通过 userID 找到评论: %s", userID)
|
logrus.Infof("✓ 通过 userID 在第 %d 个元素中找到评论: %s (尝试 %d 次)", i+1, userID, attempt+1)
|
||||||
return el, nil
|
return el, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logrus.Infof("在 %d 个元素中未找到匹配的 userID", len(elements))
|
||||||
|
} else {
|
||||||
|
logrus.Infof("获取评论元素失败或超时: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logrus.Infof("本次尝试未找到目标评论,继续下一轮...")
|
||||||
|
|
||||||
// 滚动页面
|
// === 7. 等待内容加载 ===
|
||||||
page.MustEval(`() => window.scrollBy(0, window.innerHeight * 0.8)`)
|
|
||||||
time.Sleep(scrollInterval)
|
time.Sleep(scrollInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user