- Removed redundant waiting times and improved the logic for finding comment elements and reply buttons. - Simplified the reply button search by consolidating selectors and enhancing error messages. - Improved the overall readability of the code by removing unnecessary comments and whitespace. - Ensured better handling of potential errors during the comment reply process.
561 lines
16 KiB
Go
561 lines
16 KiB
Go
package xiaohongshu
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
"github.com/go-rod/rod"
|
||
"github.com/go-rod/rod/lib/proto"
|
||
"github.com/sirupsen/logrus"
|
||
)
|
||
|
||
// CommentFeedAction 表示 Feed 评论动作
|
||
type CommentFeedAction struct {
|
||
page *rod.Page
|
||
}
|
||
|
||
// NewCommentFeedAction 创建 Feed 评论动作
|
||
func NewCommentFeedAction(page *rod.Page) *CommentFeedAction {
|
||
return &CommentFeedAction{page: page}
|
||
}
|
||
|
||
// PostComment 发表评论到 Feed
|
||
func (f *CommentFeedAction) PostComment(ctx context.Context, feedID, xsecToken, content string) error {
|
||
page := f.page.Context(ctx).Timeout(60 * time.Second)
|
||
|
||
// 构建详情页 URL
|
||
url := makeFeedDetailURL(feedID, xsecToken)
|
||
|
||
logrus.Infof("Opening feed detail page: %s", url)
|
||
|
||
// 导航到详情页
|
||
if err := page.Navigate(url); err != nil {
|
||
logrus.Warnf("Failed to navigate to feed detail page: %v", err)
|
||
return fmt.Errorf("无法打开帖子详情页,该帖子可能在网页端不可访问: %w", err)
|
||
}
|
||
|
||
if err := page.WaitStable(2 * time.Second); err != nil {
|
||
logrus.Warnf("Failed to wait for page stable: %v", err)
|
||
return fmt.Errorf("页面加载超时,该帖子可能在网页端不可访问: %w", err)
|
||
}
|
||
|
||
time.Sleep(1 * time.Second)
|
||
|
||
// 查找评论输入框
|
||
elem, err := page.Element("div.input-box div.content-edit span")
|
||
if err != nil {
|
||
logrus.Warnf("Failed to find comment input box: %v", err)
|
||
return fmt.Errorf("未找到评论输入框,该帖子可能不支持评论或网页端不可访问: %w", err)
|
||
}
|
||
|
||
if err := elem.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||
logrus.Warnf("Failed to click comment input box: %v", err)
|
||
return fmt.Errorf("无法点击评论输入框: %w", err)
|
||
}
|
||
|
||
elem2, err := page.Element("div.input-box div.content-edit p.content-input")
|
||
if err != nil {
|
||
logrus.Warnf("Failed to find comment input field: %v", err)
|
||
return fmt.Errorf("未找到评论输入区域: %w", err)
|
||
}
|
||
|
||
if err := elem2.Input(content); err != nil {
|
||
logrus.Warnf("Failed to input comment content: %v", err)
|
||
return fmt.Errorf("无法输入评论内容: %w", err)
|
||
}
|
||
|
||
time.Sleep(1 * time.Second)
|
||
|
||
submitButton, err := page.Element("div.bottom button.submit")
|
||
if err != nil {
|
||
logrus.Warnf("Failed to find submit button: %v", err)
|
||
return fmt.Errorf("未找到提交按钮: %w", err)
|
||
}
|
||
|
||
if err := submitButton.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||
logrus.Warnf("Failed to click submit button: %v", err)
|
||
return fmt.Errorf("无法点击提交按钮: %w", err)
|
||
}
|
||
|
||
time.Sleep(1 * time.Second)
|
||
|
||
logrus.Infof("Comment posted successfully to feed: %s", feedID)
|
||
return nil
|
||
}
|
||
|
||
// ReplyToComment 回复指定评论
|
||
func (f *CommentFeedAction) ReplyToComment(ctx context.Context, feedID, xsecToken, commentID, userID, content string) error {
|
||
page := f.page.Context(ctx).Timeout(60 * time.Second)
|
||
url := makeFeedDetailURL(feedID, xsecToken)
|
||
logrus.Infof("Opening feed detail page for reply: %s", url)
|
||
page.MustNavigate(url)
|
||
page.MustWaitDOMStable()
|
||
time.Sleep(3 * time.Second) // 增加等待时间确保页面完全加载
|
||
|
||
// 等待评论容器加载
|
||
waitForCommentsContainer(page)
|
||
|
||
// 确保评论区域可见
|
||
ensureCommentsVisible(page)
|
||
|
||
// 额外等待确保评论内容加载完成
|
||
time.Sleep(2 * time.Second)
|
||
|
||
// 尝试多次查找评论元素
|
||
var commentEl *rod.Element
|
||
var err error
|
||
for attempt := 0; attempt < 5; attempt++ { // 增加尝试次数
|
||
commentEl, err = findCommentElement(page, commentID, userID)
|
||
if err == nil {
|
||
break
|
||
}
|
||
logrus.Warnf("Attempt %d: Failed to find comment: %v", attempt+1, err)
|
||
time.Sleep(2 * time.Second) // 增加等待时间
|
||
ensureCommentsVisible(page)
|
||
scrollComments(page) // 每次尝试后滚动
|
||
}
|
||
|
||
if err != nil {
|
||
return fmt.Errorf("无法找到评论: %w", err)
|
||
}
|
||
|
||
// 滚动到评论位置
|
||
_, _ = commentEl.Eval(`() => { try { this.scrollIntoView({behavior: "instant", block: "center"}); } catch (e) {} return true }`)
|
||
time.Sleep(1 * time.Second) // 增加等待时间
|
||
|
||
// 尝试多次点击回复按钮
|
||
var replyBtn *rod.Element
|
||
for attempt := 0; attempt < 5; attempt++ { // 增加尝试次数
|
||
replyBtn, err = findReplyButton(commentEl)
|
||
if err == nil {
|
||
if tryClickChainForComment(replyBtn) {
|
||
break
|
||
}
|
||
}
|
||
logrus.Warnf("Attempt %d: Failed to click reply button: %v", attempt+1, err)
|
||
time.Sleep(1 * time.Second) // 增加等待时间
|
||
}
|
||
|
||
if err != nil || replyBtn == nil {
|
||
return fmt.Errorf("无法点击回复按钮")
|
||
}
|
||
|
||
time.Sleep(2 * time.Second) // 增加等待时间确保回复输入框出现
|
||
|
||
// 查找回复输入框
|
||
inputEl, err := findReplyInput(page, commentEl)
|
||
if err != nil {
|
||
return fmt.Errorf("无法找到回复输入框: %w", err)
|
||
}
|
||
|
||
// 聚焦并输入内容
|
||
if _, evalErr := inputEl.Eval(`() => { try { this.focus(); } catch (e) {} return true }`); evalErr != nil {
|
||
logrus.Warnf("focus reply input failed: %v", evalErr)
|
||
}
|
||
|
||
inputEl.MustInput(content)
|
||
time.Sleep(500 * time.Millisecond) // 增加等待时间
|
||
|
||
// 查找并点击提交按钮
|
||
submitBtn, err := findSubmitButton(page)
|
||
if err != nil {
|
||
return fmt.Errorf("无法找到提交按钮: %w", err)
|
||
}
|
||
|
||
if !tryClickChainForComment(submitBtn) {
|
||
return fmt.Errorf("点击回复提交按钮失败")
|
||
}
|
||
|
||
time.Sleep(3 * time.Second) // 增加等待时间确保回复提交完成
|
||
return nil
|
||
}
|
||
|
||
func findCommentElement(page *rod.Page, commentID, userID string) (*rod.Element, error) {
|
||
var lastErr error
|
||
|
||
// 首先尝试确保评论区域可见
|
||
ensureCommentsVisible(page)
|
||
|
||
for attempt := 0; attempt < 20; attempt++ { // 增加尝试次数
|
||
logrus.Infof("查找评论,尝试次数: %d", attempt+1)
|
||
el, err := locateCommentElement(page, commentID, userID)
|
||
if err == nil && el != nil {
|
||
logrus.Infof("成功找到评论")
|
||
return el, nil
|
||
}
|
||
if err != nil {
|
||
lastErr = err
|
||
}
|
||
|
||
// 每3次尝试后进行一次更彻底的滚动
|
||
if attempt%3 == 0 {
|
||
// 更彻底的滚动策略
|
||
performFullScroll(page)
|
||
} else {
|
||
// 常规滚动
|
||
if !scrollComments(page) {
|
||
logrus.Infof("滚动到底部,无法继续滚动")
|
||
break
|
||
}
|
||
}
|
||
time.Sleep(800 * time.Millisecond) // 增加等待时间
|
||
}
|
||
|
||
if lastErr != nil {
|
||
return nil, lastErr
|
||
}
|
||
return nil, fmt.Errorf("未找到评论: %s", buildIdentifier(commentID, userID))
|
||
}
|
||
|
||
func locateCommentElement(page *rod.Page, commentID, userID string) (*rod.Element, error) {
|
||
// 如果在comments-container内没有找到,尝试在整个页面查找
|
||
if commentID != "" {
|
||
if el, err := locateCommentElementByCommentID(page, commentID); err == nil && el != nil {
|
||
return el, nil
|
||
}
|
||
}
|
||
if userID != "" {
|
||
if el, err := locateCommentElementByUserID(page, userID); err == nil && el != nil {
|
||
return el, nil
|
||
}
|
||
}
|
||
|
||
identifier := buildIdentifier(commentID, userID)
|
||
if identifier == "" {
|
||
return nil, fmt.Errorf("未提供评论标识")
|
||
}
|
||
return nil, fmt.Errorf("未找到评论: %s", identifier)
|
||
}
|
||
|
||
func locateCommentElementByCommentID(page *rod.Page, commentID string) (*rod.Element, error) {
|
||
if commentID == "" {
|
||
return nil, fmt.Errorf("评论ID为空")
|
||
}
|
||
|
||
// 首先尝试直接通过ID查找(根据HTML结构中的id="comment-68d9df3e0000000002015818")
|
||
idSelector := fmt.Sprintf("#comment-%s", commentID)
|
||
if el, err := page.Element(idSelector); err == nil && el != nil {
|
||
return el, nil
|
||
}
|
||
|
||
return nil, fmt.Errorf("未找到评论ID: %s", commentID)
|
||
}
|
||
|
||
func locateCommentElementByUserID(page *rod.Page, userID string) (*rod.Element, error) {
|
||
if userID == "" {
|
||
return nil, fmt.Errorf("用户ID为空")
|
||
}
|
||
|
||
selectors := []string{
|
||
fmt.Sprintf(`[data-user-id="%s"]`, userID),
|
||
}
|
||
|
||
for _, selector := range selectors {
|
||
if el, err := page.Element(selector); err == nil && el != nil {
|
||
// 使用JavaScript查找父级评论元素
|
||
jsCode := `() => {
|
||
let current = this;
|
||
while (current) {
|
||
if (current.classList && (current.classList.contains('comment-item') || current.classList.contains('comment'))) {
|
||
return current;
|
||
}
|
||
current = current.parentElement;
|
||
}
|
||
return this;
|
||
}`
|
||
if _, err := el.Eval(jsCode); err == nil {
|
||
return el, nil
|
||
}
|
||
return el, nil
|
||
}
|
||
}
|
||
|
||
return nil, fmt.Errorf("未找到用户ID: %s", userID)
|
||
}
|
||
|
||
// 等待评论容器加载完成
|
||
func waitForCommentsContainer(page *rod.Page) {
|
||
jsCode := `() => {
|
||
// 等待comments-container元素出现
|
||
let attempts = 0;
|
||
const maxAttempts = 10;
|
||
|
||
const checkContainer = () => {
|
||
const container = document.querySelector('.comments-container');
|
||
if (container) {
|
||
// 检查容器内是否有评论内容
|
||
const comments = container.querySelectorAll('.comment-item, .comment');
|
||
return comments.length > 0;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
// 定期检查评论容器是否加载完成
|
||
const interval = setInterval(() => {
|
||
attempts++;
|
||
if (checkContainer() || attempts >= maxAttempts) {
|
||
clearInterval(interval);
|
||
}
|
||
}, 500);
|
||
|
||
return checkContainer();
|
||
}`
|
||
|
||
page.Eval(jsCode)
|
||
time.Sleep(2 * time.Second) // 等待检查完成
|
||
}
|
||
|
||
func ensureCommentsVisible(page *rod.Page) {
|
||
// 专门针对comments-container元素的JavaScript代码
|
||
jsCode := `() => {
|
||
// 查找comments-container元素
|
||
const commentsContainer = document.querySelector('.comments-container');
|
||
|
||
// 如果找到comments-container,尝试滚动到视图中并在其内部滚动
|
||
if (commentsContainer) {
|
||
// 先滚动到视图中
|
||
commentsContainer.scrollIntoView({behavior: 'instant', block: 'start'});
|
||
|
||
// 等待一下再在容器内部滚动
|
||
setTimeout(() => {
|
||
// 在comments-container内部滚动以显示评论
|
||
if (commentsContainer.scrollHeight > commentsContainer.clientHeight) {
|
||
const maxScroll = commentsContainer.scrollHeight - commentsContainer.clientHeight;
|
||
if (maxScroll > 0) {
|
||
// 滚动到一半位置
|
||
commentsContainer.scrollTop = Math.min(maxScroll, commentsContainer.clientHeight * 0.5);
|
||
}
|
||
}
|
||
}, 200);
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}`
|
||
|
||
page.Eval(jsCode)
|
||
time.Sleep(1 * time.Second)
|
||
}
|
||
|
||
func scrollComments(page *rod.Page) bool {
|
||
scrollJS := `() => {
|
||
let scrolled = false;
|
||
|
||
// 专门查找comments-container元素
|
||
const commentsContainer = document.querySelector('.comments-container');
|
||
|
||
if (commentsContainer) {
|
||
const maxScroll = commentsContainer.scrollHeight - commentsContainer.clientHeight;
|
||
if (maxScroll > 0 && commentsContainer.scrollTop < maxScroll) {
|
||
// 滚动更多内容
|
||
const delta = Math.max(commentsContainer.clientHeight * 0.8, 400);
|
||
commentsContainer.scrollTop = Math.min(maxScroll, commentsContainer.scrollTop + delta);
|
||
scrolled = true;
|
||
}
|
||
}
|
||
|
||
return scrolled;
|
||
}`
|
||
res, err := page.Eval(scrollJS)
|
||
if err != nil {
|
||
logrus.Warnf("scroll comments failed: %v", err)
|
||
return false
|
||
}
|
||
if res == nil {
|
||
return false
|
||
}
|
||
return res.Value.Bool()
|
||
}
|
||
|
||
// performFullScroll 执行更彻底的滚动策略
|
||
func performFullScroll(page *rod.Page) {
|
||
logrus.Infof("执行彻底滚动策略")
|
||
|
||
// 策略1: 滚动到评论容器的不同位置
|
||
scrollPositionsJS := `() => {
|
||
const commentsContainer = document.querySelector('.comments-container');
|
||
if (!commentsContainer) return false;
|
||
|
||
const maxScroll = commentsContainer.scrollHeight - commentsContainer.clientHeight;
|
||
if (maxScroll <= 0) return false;
|
||
|
||
// 根据当前滚动位置决定下一步滚动
|
||
const currentScroll = commentsContainer.scrollTop;
|
||
const scrollRatio = currentScroll / maxScroll;
|
||
|
||
if (scrollRatio < 0.3) {
|
||
// 滚动到30%位置
|
||
commentsContainer.scrollTop = maxScroll * 0.3;
|
||
} else if (scrollRatio < 0.6) {
|
||
// 滚动到60%位置
|
||
commentsContainer.scrollTop = maxScroll * 0.6;
|
||
} else if (scrollRatio < 0.9) {
|
||
// 滚动到90%位置
|
||
commentsContainer.scrollTop = maxScroll * 0.9;
|
||
} else {
|
||
// 滚动到底部
|
||
commentsContainer.scrollTop = maxScroll;
|
||
}
|
||
|
||
return true;
|
||
}`
|
||
|
||
if _, err := page.Eval(scrollPositionsJS); err != nil {
|
||
logrus.Warnf("彻底滚动失败: %v", err)
|
||
}
|
||
|
||
}
|
||
|
||
func buildIdentifier(commentID, userID string) string {
|
||
if commentID != "" && userID != "" {
|
||
return fmt.Sprintf("comment_id=%s / user_id=%s", commentID, userID)
|
||
}
|
||
if commentID != "" {
|
||
return commentID
|
||
}
|
||
return userID
|
||
}
|
||
|
||
// 选取当前层主的回复按钮
|
||
func findReplyButton(commentEl *rod.Element) (*rod.Element, error) {
|
||
if commentEl == nil {
|
||
return nil, fmt.Errorf("评论元素为空")
|
||
}
|
||
|
||
selector := ".right .interactions .reply"
|
||
btn, err := commentEl.Element(selector)
|
||
if err != nil || btn == nil {
|
||
logrus.Warnf("未找到回复按钮,选择器: %s, err: %v", selector, err)
|
||
return nil, fmt.Errorf("未找到回复按钮")
|
||
}
|
||
|
||
logrus.Infof("通过选择器 %s 找到回复按钮", selector)
|
||
return btn, nil
|
||
}
|
||
|
||
// verifyClickSuccess 验证点击是否真的成功(检查是否出现了回复输入框)
|
||
func verifyClickSuccess(clickedEl *rod.Element) bool {
|
||
// 获取页面实例
|
||
page := clickedEl.Page()
|
||
|
||
// 检查是否出现了回复输入框
|
||
selectors := []string{
|
||
"div.input-box div.content-edit p.content-input",
|
||
}
|
||
|
||
for _, selector := range selectors {
|
||
if el, err := page.Element(selector); err == nil && el != nil {
|
||
// 检查元素是否可见
|
||
if visible, _ := el.Visible(); visible {
|
||
logrus.Infof("验证成功:找到可见的回复输入框 (%s)", selector)
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
logrus.Infof("验证失败:没有找到回复输入框")
|
||
return false
|
||
}
|
||
|
||
func findReplyInput(page *rod.Page, commentEl *rod.Element) (*rod.Element, error) {
|
||
activeEditableJS := `() => {
|
||
const active = document.activeElement;
|
||
if (active && active.getAttribute && active.getAttribute('contenteditable') === 'true') {
|
||
return active;
|
||
}
|
||
return null;
|
||
}`
|
||
if el, err := page.ElementByJS(rod.Eval(activeEditableJS)); err == nil && el != nil {
|
||
return el, nil
|
||
}
|
||
selectors := []string{
|
||
"div.input-box div.content-edit p.content-input", // 原有选择器
|
||
}
|
||
for _, selector := range selectors {
|
||
if el, err := page.Element(selector); err == nil && el != nil {
|
||
return el, nil
|
||
}
|
||
}
|
||
return nil, fmt.Errorf("未找到回复输入框")
|
||
}
|
||
|
||
func tryClickChainForComment(el *rod.Element) bool {
|
||
if el == nil {
|
||
logrus.Errorf("要点击的元素为空")
|
||
return false
|
||
}
|
||
|
||
// 获取元素信息用于调试
|
||
text, _ := el.Text()
|
||
classAttr, _ := el.Attribute("class")
|
||
class := ""
|
||
if classAttr != nil {
|
||
class = *classAttr
|
||
}
|
||
tagName := ""
|
||
if desc, err := el.Describe(0, false); err == nil && desc != nil {
|
||
tagName = desc.NodeName
|
||
}
|
||
logrus.Infof("准备点击元素 - 文本: '%s', 类: '%s', 标签: %s", text, class, tagName)
|
||
|
||
// 检查元素是否可见和可点击
|
||
visible, _ := el.Visible()
|
||
logrus.Infof("元素可见性: %v", visible)
|
||
|
||
// 滚动到元素位置
|
||
_, _ = el.Eval(`() => { try { this.scrollIntoView({behavior: "instant", block: "center"}); } catch (e) {} return true }`)
|
||
time.Sleep(500 * time.Millisecond)
|
||
|
||
// 只使用直接点击方式
|
||
clickMethods := []struct {
|
||
name string
|
||
fn func(*rod.Element) bool
|
||
}{
|
||
{"直接点击", func(e *rod.Element) bool {
|
||
if err := e.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||
logrus.Warnf("直接点击失败: %v", err)
|
||
return false
|
||
}
|
||
logrus.Infof("直接点击成功")
|
||
return true
|
||
}},
|
||
}
|
||
|
||
for i, method := range clickMethods {
|
||
logrus.Infof("尝试点击方法 %d: %s", i+1, method.name)
|
||
if method.fn(el) {
|
||
// 点击后等待一下,检查是否有反应
|
||
time.Sleep(1 * time.Second)
|
||
|
||
// 验证点击是否真的成功(检查是否出现了回复输入框)
|
||
success := verifyClickSuccess(el)
|
||
if success {
|
||
logrus.Infof("点击方法 %s 执行成功且有效", method.name)
|
||
return true
|
||
} else {
|
||
logrus.Warnf("点击方法 %s 执行成功但无效(没有出现回复输入框)", method.name)
|
||
// 继续尝试下一种方法
|
||
}
|
||
}
|
||
}
|
||
|
||
logrus.Errorf("所有点击方法都失败")
|
||
return false
|
||
}
|
||
|
||
func findSubmitButton(page *rod.Page) (*rod.Element, error) {
|
||
selectors := []string{
|
||
"div.bottom button.submit",
|
||
}
|
||
for _, selector := range selectors {
|
||
if el, err := page.Element(selector); err == nil && el != nil {
|
||
disabled, _ := el.Attribute("disabled")
|
||
if disabled == nil {
|
||
return el, nil
|
||
}
|
||
}
|
||
}
|
||
return nil, fmt.Errorf("未找到回复发布按钮")
|
||
}
|