fix: 修复滚动滑动回复问题
This commit is contained in:
@@ -24,12 +24,9 @@ func NewCommentFeedAction(page *rod.Page) *CommentFeedAction {
|
||||
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("打开 feed 详情页: %s", url)
|
||||
|
||||
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)
|
||||
@@ -42,7 +39,6 @@ func (f *CommentFeedAction) PostComment(ctx context.Context, feedID, xsecToken,
|
||||
|
||||
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)
|
||||
@@ -86,62 +82,52 @@ func (f *CommentFeedAction) PostComment(ctx context.Context, feedID, xsecToken,
|
||||
|
||||
// ReplyToComment 回复指定评论
|
||||
func (f *CommentFeedAction) ReplyToComment(ctx context.Context, feedID, xsecToken, commentID, userID, content string) error {
|
||||
page := f.page.Context(ctx).Timeout(60 * time.Second)
|
||||
// 增加超时时间,因为需要滚动查找评论
|
||||
page := f.page.Context(ctx).Timeout(5 * time.Minute)
|
||||
url := makeFeedDetailURL(feedID, xsecToken)
|
||||
logrus.Infof("Opening feed detail page for reply: %s", url)
|
||||
logrus.Infof("打开 feed 详情页进行回复: %s", url)
|
||||
|
||||
page.MustNavigate(url)
|
||||
page.MustWaitDOMStable()
|
||||
time.Sleep(3 * time.Second) // 增加等待时间确保页面完全加载
|
||||
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) // 每次尝试后滚动
|
||||
}
|
||||
|
||||
// 使用新的查找逻辑(完全在 JS 中执行)
|
||||
commentEl, err := findCommentElementNew(page, commentID, userID)
|
||||
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) // 增加等待时间
|
||||
// 多次滚动确保可见
|
||||
for i := 0; i < 3; i++ {
|
||||
logrus.Infof("第 %d 次滚动到评论位置...", i+1)
|
||||
_, _ = commentEl.Eval(`() => {
|
||||
this.scrollIntoView({behavior: "instant", block: "center"});
|
||||
return true
|
||||
}`)
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
|
||||
// 尝试多次点击回复按钮
|
||||
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) // 增加等待时间
|
||||
// 往下多滚动一点
|
||||
page.MustEval(`() => window.scrollBy(0, 150)`)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
if err != nil || replyBtn == nil {
|
||||
return fmt.Errorf("无法点击回复按钮")
|
||||
logrus.Info("滚动完成,准备点击回复按钮")
|
||||
|
||||
// 查找并点击回复按钮
|
||||
replyBtn, err := findReplyButton(commentEl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法找到回复按钮: %w", err)
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second) // 增加等待时间确保回复输入框出现
|
||||
if !tryClickChainForComment(replyBtn) {
|
||||
return fmt.Errorf("点击回复按钮失败")
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 查找回复输入框
|
||||
inputEl, err := findReplyInput(page, commentEl)
|
||||
@@ -150,12 +136,17 @@ func (f *CommentFeedAction) ReplyToComment(ctx context.Context, feedID, xsecToke
|
||||
}
|
||||
|
||||
// 聚焦并输入内容
|
||||
if _, evalErr := inputEl.Eval(`() => { try { this.focus(); } catch (e) {} return true }`); evalErr != nil {
|
||||
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) // 增加等待时间
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// 查找并点击提交按钮
|
||||
submitBtn, err := findSubmitButton(page)
|
||||
@@ -167,131 +158,368 @@ func (f *CommentFeedAction) ReplyToComment(ctx context.Context, feedID, xsecToke
|
||||
return fmt.Errorf("点击回复提交按钮失败")
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second) // 增加等待时间确保回复提交完成
|
||||
time.Sleep(3 * time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
func findCommentElement(page *rod.Page, commentID, userID string) (*rod.Element, error) {
|
||||
var lastErr error
|
||||
func findCommentElementNew(page *rod.Page, commentID, userID string) (*rod.Element, error) {
|
||||
logrus.Infof("🔍 开始查找评论(新方法)- commentID: %s, userID: %s", commentID, userID)
|
||||
|
||||
// 首先尝试确保评论区域可见
|
||||
ensureCommentsVisible(page)
|
||||
// 修改 JS:找到后记录元素的 ID
|
||||
findCommentJS := fmt.Sprintf(`async () => {
|
||||
const INTERVAL_MS = 900;
|
||||
const STAGNANT_LIMIT = 8;
|
||||
const NO_CHANGE_SCROLL_LIMIT = 3;
|
||||
const DELTA_MIN = 480;
|
||||
const SCROLL_TIMEOUT = 900;
|
||||
const MAX_ATTEMPTS = 100;
|
||||
const CLICK_MORE_INTERVAL = 2;
|
||||
const CLICK_WAIT_TIME = 300;
|
||||
|
||||
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
|
||||
}
|
||||
const TARGET_COMMENT_ID = %q;
|
||||
const TARGET_USER_ID = %q;
|
||||
|
||||
// 每3次尝试后进行一次更彻底的滚动
|
||||
if attempt%3 == 0 {
|
||||
// 更彻底的滚动策略
|
||||
performFullScroll(page)
|
||||
} else {
|
||||
// 常规滚动
|
||||
if !scrollComments(page) {
|
||||
logrus.Infof("滚动到底部,无法继续滚动")
|
||||
break
|
||||
console.log('开始查找评论 - TARGET_COMMENT_ID:', TARGET_COMMENT_ID, 'TARGET_USER_ID:', TARGET_USER_ID);
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const scrollRoot = () => document.scrollingElement || document.documentElement || document.body;
|
||||
const getContainer = () => document.querySelector('.comments-container');
|
||||
|
||||
const clickShowMoreButtons = () => {
|
||||
let clickedCount = 0;
|
||||
const elements = document.querySelectorAll('.show-more');
|
||||
|
||||
elements.forEach((el) => {
|
||||
try {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
const isVisible = (
|
||||
rect.height > 0 &&
|
||||
rect.width > 0 &&
|
||||
style.display !== 'none' &&
|
||||
style.visibility !== 'hidden' &&
|
||||
style.opacity !== '0' &&
|
||||
rect.top < window.innerHeight + 500 &&
|
||||
rect.bottom > -500
|
||||
);
|
||||
|
||||
if (isVisible) {
|
||||
el.click();
|
||||
clickedCount++;
|
||||
}
|
||||
} catch (err) {
|
||||
console.debug('点击失败', err);
|
||||
}
|
||||
});
|
||||
|
||||
return clickedCount;
|
||||
};
|
||||
|
||||
// === 修改:返回元素的稳定标识符 ===
|
||||
const findTargetComment = () => {
|
||||
// 优先通过 commentID 查找
|
||||
if (TARGET_COMMENT_ID) {
|
||||
const byId = document.querySelector('#comment-' + TARGET_COMMENT_ID);
|
||||
if (byId) {
|
||||
console.log('通过 commentID 找到评论:', TARGET_COMMENT_ID);
|
||||
// 返回包含完整信息的对象
|
||||
return {
|
||||
element: byId,
|
||||
selector: '#comment-' + TARGET_COMMENT_ID,
|
||||
commentId: TARGET_COMMENT_ID
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
time.Sleep(800 * time.Millisecond) // 增加等待时间
|
||||
}
|
||||
|
||||
// 通过 userID 查找
|
||||
if (TARGET_USER_ID) {
|
||||
const allComments = document.querySelectorAll('.comment-item, .comment');
|
||||
for (const comment of allComments) {
|
||||
const userIdEl = comment.querySelector('[data-user-id="' + TARGET_USER_ID + '"]');
|
||||
if (userIdEl) {
|
||||
console.log('通过 userID 找到评论:', TARGET_USER_ID);
|
||||
|
||||
// 尝试获取评论的 ID
|
||||
const commentId = comment.id;
|
||||
if (commentId) {
|
||||
return {
|
||||
element: comment,
|
||||
selector: '#' + commentId,
|
||||
commentId: commentId.replace('comment-', '')
|
||||
};
|
||||
} else {
|
||||
// 如果没有 ID,给它添加一个唯一标识
|
||||
const uniqueId = 'xhs-found-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
comment.id = uniqueId;
|
||||
return {
|
||||
element: comment,
|
||||
selector: '#' + uniqueId,
|
||||
commentId: null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
if lastErr != nil {
|
||||
return nil, lastErr
|
||||
}
|
||||
return nil, fmt.Errorf("未找到评论: %s", buildIdentifier(commentID, userID))
|
||||
}
|
||||
// ... (保留原有的滚动逻辑) ...
|
||||
const getScrollMetrics = (el) => {
|
||||
if (!el) {
|
||||
return { top: 0, max: 0, client: window.innerHeight };
|
||||
}
|
||||
if (el === window || el === document || el === document.body || el === document.documentElement) {
|
||||
const root = scrollRoot();
|
||||
return {
|
||||
top: root.scrollTop,
|
||||
max: Math.max(root.scrollHeight - root.clientHeight, 0),
|
||||
client: root.clientHeight || window.innerHeight
|
||||
};
|
||||
}
|
||||
return {
|
||||
top: el.scrollTop,
|
||||
max: Math.max(el.scrollHeight - el.clientHeight, 0),
|
||||
client: el.clientHeight
|
||||
};
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
const setScrollTop = (el, value) => {
|
||||
if (!el) return;
|
||||
if (el === window || el === document || el === document.body || el === document.documentElement) {
|
||||
const root = scrollRoot();
|
||||
root.scrollTop = value;
|
||||
window.scrollTo(0, value);
|
||||
return;
|
||||
}
|
||||
el.scrollTop = value;
|
||||
};
|
||||
|
||||
identifier := buildIdentifier(commentID, userID)
|
||||
if identifier == "" {
|
||||
return nil, fmt.Errorf("未提供评论标识")
|
||||
}
|
||||
return nil, fmt.Errorf("未找到评论: %s", identifier)
|
||||
}
|
||||
const dispatchWheel = (el, delta) => {
|
||||
if (!el) return;
|
||||
try {
|
||||
const wheel = new WheelEvent('wheel', {
|
||||
deltaY: delta,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
});
|
||||
el.dispatchEvent(wheel);
|
||||
el.dispatchEvent(new Event('scroll', { bubbles: true }));
|
||||
} catch (err) {
|
||||
console.debug('dispatchWheel error', err);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
const findScrollTarget = () => {
|
||||
const container = getContainer();
|
||||
const candidates = new Set();
|
||||
|
||||
if (container) {
|
||||
let current = container;
|
||||
while (current) {
|
||||
if (current.classList && (current.classList.contains('comment-item') || current.classList.contains('comment'))) {
|
||||
return current;
|
||||
if (current instanceof HTMLElement) {
|
||||
candidates.add(current);
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
return this;
|
||||
}`
|
||||
if _, err := el.Eval(jsCode); err == nil {
|
||||
return el, nil
|
||||
}
|
||||
return el, nil
|
||||
|
||||
candidates.add(document.body);
|
||||
candidates.add(document.documentElement);
|
||||
|
||||
const weighted = Array.from(candidates).map((node) => {
|
||||
const style = window.getComputedStyle(node);
|
||||
const overflowY = style.overflowY;
|
||||
const scrollable = node.scrollHeight - node.clientHeight > 40;
|
||||
const hasScrollStyle = /auto|scroll|overlay/i.test(overflowY);
|
||||
const weight =
|
||||
(node.contains(container) ? 1000 : 0) +
|
||||
(node === container ? 800 : 0) +
|
||||
(hasScrollStyle ? 400 : 0) +
|
||||
(scrollable ? 300 : 0) -
|
||||
(node === document.body || node === document.documentElement ? 50 : 0);
|
||||
|
||||
if (scrollable || hasScrollStyle || node === document.body || node === document.documentElement) {
|
||||
return { node, weight };
|
||||
}
|
||||
return null;
|
||||
}).filter(Boolean);
|
||||
|
||||
weighted.sort((a, b) => b.weight - a.weight);
|
||||
|
||||
return weighted.length > 0 ? weighted[0].node : scrollRoot();
|
||||
};
|
||||
|
||||
const performScroll = (target) => {
|
||||
const scrollTarget = target || findScrollTarget();
|
||||
if (!scrollTarget) {
|
||||
window.scrollBy(0, window.innerHeight * 0.8);
|
||||
return;
|
||||
}
|
||||
|
||||
const metrics = getScrollMetrics(scrollTarget);
|
||||
const beforeTop = metrics.top;
|
||||
const desired = metrics.max > 0
|
||||
? Math.min(metrics.top + Math.max(metrics.client * 0.85, DELTA_MIN), metrics.max)
|
||||
: metrics.top + Math.max(metrics.client * 0.85, DELTA_MIN);
|
||||
const applied = Math.max(0, desired - metrics.top);
|
||||
|
||||
setScrollTop(scrollTarget, desired);
|
||||
dispatchWheel(scrollTarget, applied);
|
||||
|
||||
const afterTop = getScrollMetrics(scrollTarget).top;
|
||||
if (Math.abs(afterTop - beforeTop) < 5 && scrollTarget !== scrollRoot()) {
|
||||
const root = scrollRoot();
|
||||
const rootBefore = root.scrollTop;
|
||||
root.scrollTop = rootBefore + applied;
|
||||
window.scrollBy(0, applied);
|
||||
dispatchWheel(root, applied);
|
||||
}
|
||||
};
|
||||
|
||||
// 主查找逻辑
|
||||
let lastScrollTop = 0;
|
||||
let stagnantChecks = 0;
|
||||
let noScrollChangeCount = 0;
|
||||
let totalClickedButtons = 0;
|
||||
|
||||
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
||||
const container = getContainer();
|
||||
if (!container) {
|
||||
await sleep(300);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attempt %% CLICK_MORE_INTERVAL === 0) {
|
||||
const clicked = clickShowMoreButtons();
|
||||
if (clicked > 0) {
|
||||
totalClickedButtons += clicked;
|
||||
console.log('点击了 ' + clicked + ' 个"更多"按钮,累计: ' + totalClickedButtons);
|
||||
await sleep(CLICK_WAIT_TIME);
|
||||
|
||||
await sleep(200);
|
||||
const clicked2 = clickShowMoreButtons();
|
||||
if (clicked2 > 0) {
|
||||
totalClickedButtons += clicked2;
|
||||
console.log('二次检查点击了 ' + clicked2 + ' 个"更多"按钮');
|
||||
await sleep(CLICK_WAIT_TIME);
|
||||
}
|
||||
|
||||
const foundInfo = findTargetComment();
|
||||
if (foundInfo) {
|
||||
console.log('点击"更多"后找到评论,总共点击了 ' + totalClickedButtons + ' 个按钮');
|
||||
return {
|
||||
status: 'found',
|
||||
attempts: attempt + 1,
|
||||
clickedButtons: totalClickedButtons,
|
||||
selector: foundInfo.selector,
|
||||
commentId: foundInfo.commentId
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const foundInfo = findTargetComment();
|
||||
if (foundInfo) {
|
||||
console.log('找到评论,尝试次数: ' + (attempt + 1) + ',总共点击了 ' + totalClickedButtons + ' 个按钮');
|
||||
return {
|
||||
status: 'found',
|
||||
attempts: attempt + 1,
|
||||
clickedButtons: totalClickedButtons,
|
||||
selector: foundInfo.selector,
|
||||
commentId: foundInfo.commentId
|
||||
};
|
||||
}
|
||||
|
||||
const target = findScrollTarget();
|
||||
const beforeTop = getScrollMetrics(target).top;
|
||||
performScroll(target);
|
||||
await sleep(SCROLL_TIMEOUT);
|
||||
const afterTop = getScrollMetrics(target).top;
|
||||
|
||||
if (Math.abs(afterTop - beforeTop) < 5) {
|
||||
noScrollChangeCount += 1;
|
||||
} else {
|
||||
noScrollChangeCount = 0;
|
||||
lastScrollTop = afterTop;
|
||||
}
|
||||
|
||||
if (noScrollChangeCount >= NO_CHANGE_SCROLL_LIMIT) {
|
||||
return { status: 'not_found', reason: 'no-scroll-change', attempts: attempt + 1, clickedButtons: totalClickedButtons };
|
||||
}
|
||||
|
||||
if (INTERVAL_MS > SCROLL_TIMEOUT) {
|
||||
await sleep(INTERVAL_MS - SCROLL_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
return { status: 'not_found', reason: 'timeout', attempts: MAX_ATTEMPTS, clickedButtons: totalClickedButtons };
|
||||
}`, commentID, userID)
|
||||
|
||||
// 执行 JS
|
||||
result, err := page.Eval(findCommentJS)
|
||||
if err != nil {
|
||||
logrus.Errorf("执行查找评论 JS 失败: %v", err)
|
||||
return nil, fmt.Errorf("执行查找评论 JS 失败: %w", err)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("未找到用户ID: %s", userID)
|
||||
}
|
||||
// 解析结果
|
||||
resultJSON, err := page.ObjectToJSON(result)
|
||||
if err != nil {
|
||||
logrus.Errorf("无法将结果转换为 JSON: %v", err)
|
||||
return nil, fmt.Errorf("无法将结果转换为 JSON: %w", err)
|
||||
}
|
||||
|
||||
// 等待评论容器加载完成
|
||||
status := resultJSON.Get("status").Str()
|
||||
reason := resultJSON.Get("reason").Str()
|
||||
attempts := resultJSON.Get("attempts").Int()
|
||||
clickedButtons := resultJSON.Get("clickedButtons").Int()
|
||||
selector := resultJSON.Get("selector").Str()
|
||||
|
||||
logrus.Infof("查找结果: status=%s, reason=%s, attempts=%d, clickedButtons=%d, selector=%s",
|
||||
status, reason, attempts, clickedButtons, selector)
|
||||
|
||||
if status != "found" {
|
||||
return nil, fmt.Errorf("未找到评论 (commentID: %s, userID: %s), 原因: %s, 尝试次数: %d, 点击按钮: %d",
|
||||
commentID, userID, reason, attempts, clickedButtons)
|
||||
}
|
||||
|
||||
// === 关键修改:使用返回的稳定选择器而不是临时标记 ===
|
||||
el, err := page.Element(selector)
|
||||
if err != nil {
|
||||
logrus.Errorf("找到评论但无法获取元素,选择器: %s, 错误: %v", selector, err)
|
||||
|
||||
// 如果稳定选择器失败,尝试重新查找
|
||||
logrus.Info("尝试通过 commentID 重新查找...")
|
||||
if commentID != "" {
|
||||
fallbackSelector := fmt.Sprintf("#comment-%s", commentID)
|
||||
el, err = page.Element(fallbackSelector)
|
||||
if err == nil {
|
||||
logrus.Infof("通过备用选择器 %s 成功找到元素", fallbackSelector)
|
||||
return el, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("找到评论但无法获取元素: %w", err)
|
||||
}
|
||||
|
||||
logrus.Infof("✓ 成功获取评论元素,选择器: %s", selector)
|
||||
return el, nil
|
||||
}
|
||||
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) {
|
||||
@@ -303,122 +531,9 @@ func waitForCommentsContainer(page *rod.Page) {
|
||||
}`
|
||||
|
||||
page.Eval(jsCode)
|
||||
time.Sleep(2 * time.Second) // 等待检查完成
|
||||
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("评论元素为空")
|
||||
@@ -435,19 +550,14 @@ func findReplyButton(commentEl *rod.Element) (*rod.Element, error) {
|
||||
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
|
||||
@@ -460,17 +570,18 @@ func verifyClickSuccess(clickedEl *rod.Element) bool {
|
||||
|
||||
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;
|
||||
}`
|
||||
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", // 原有选择器
|
||||
"div.input-box div.content-edit p.content-input",
|
||||
}
|
||||
for _, selector := range selectors {
|
||||
if el, err := page.Element(selector); err == nil && el != nil {
|
||||
@@ -486,7 +597,6 @@ func tryClickChainForComment(el *rod.Element) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取元素信息用于调试
|
||||
text, _ := el.Text()
|
||||
classAttr, _ := el.Attribute("class")
|
||||
class := ""
|
||||
@@ -499,48 +609,32 @@ func tryClickChainForComment(el *rod.Element) bool {
|
||||
}
|
||||
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 }`)
|
||||
_, _ = 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
|
||||
}},
|
||||
if err := el.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||
logrus.Warnf("点击失败: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
for i, method := range clickMethods {
|
||||
logrus.Infof("尝试点击方法 %d: %s", i+1, method.name)
|
||||
if method.fn(el) {
|
||||
// 点击后等待一下,检查是否有反应
|
||||
time.Sleep(1 * time.Second)
|
||||
logrus.Infof("点击成功")
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// 验证点击是否真的成功(检查是否出现了回复输入框)
|
||||
success := verifyClickSuccess(el)
|
||||
if success {
|
||||
logrus.Infof("点击方法 %s 执行成功且有效", method.name)
|
||||
return true
|
||||
} else {
|
||||
logrus.Warnf("点击方法 %s 执行成功但无效(没有出现回复输入框)", method.name)
|
||||
// 继续尝试下一种方法
|
||||
}
|
||||
}
|
||||
success := verifyClickSuccess(el)
|
||||
if success {
|
||||
logrus.Infof("点击执行成功且有效")
|
||||
return true
|
||||
}
|
||||
|
||||
logrus.Errorf("所有点击方法都失败")
|
||||
logrus.Warnf("点击执行成功但无效(没有出现回复输入框)")
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,56 @@ func (f *FeedDetailAction) GetFeedDetail(ctx context.Context, feedID, xsecToken
|
||||
page.MustWaitDOMStable()
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// === 检测「笔记暂时无法浏览」或类似不可访问页面 ===
|
||||
unavailableResult := page.MustEval(`() => {
|
||||
const wrapper = document.querySelector('.access-wrapper, .error-wrapper, .not-found-wrapper, .blocked-wrapper');
|
||||
if (!wrapper) return null;
|
||||
|
||||
const text = wrapper.textContent || '';
|
||||
const keywords = [
|
||||
'当前笔记暂时无法浏览',
|
||||
'该内容因违规已被删除',
|
||||
'该笔记已被删除',
|
||||
'内容不存在',
|
||||
'笔记不存在',
|
||||
'已失效',
|
||||
'私密笔记',
|
||||
'仅作者可见',
|
||||
'因用户设置,你无法查看',
|
||||
'因违规无法查看',
|
||||
'这是一片荒地点击评论'
|
||||
];
|
||||
|
||||
for (const kw of keywords) {
|
||||
if (text.includes(kw)) {
|
||||
return kw.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}`)
|
||||
|
||||
// The result is a gson.JSON object. We need to get its raw JSON representation to check for "null".
|
||||
rawJSON, err := unavailableResult.MarshalJSON()
|
||||
if err != nil {
|
||||
logrus.Errorf("无法解析页面状态检查的结果: %v", err)
|
||||
return nil, fmt.Errorf("无法解析页面状态检查的结果: %w", err)
|
||||
}
|
||||
|
||||
if string(rawJSON) != "null" {
|
||||
var reason string
|
||||
// JS 返回的字符串会被 JSON 编码,所以需要 Unmarshal
|
||||
if err := json.Unmarshal(rawJSON, &reason); err == nil {
|
||||
logrus.Warnf("笔记不可访问: %s", reason)
|
||||
return nil, fmt.Errorf("笔记不可访问: %s", reason)
|
||||
} else {
|
||||
// 如果解析失败,直接使用原始值
|
||||
rawReason := string(rawJSON)
|
||||
logrus.Warnf("笔记不可访问,且无法解析原因: %s", rawReason)
|
||||
return nil, fmt.Errorf("笔记不可访问,无法解析原因: %s", rawReason)
|
||||
}
|
||||
}
|
||||
|
||||
// === 加载全部评论(简化版本)===
|
||||
if loadAllComments {
|
||||
scrollAllCommentsJS := `() => {
|
||||
const INTERVAL_MS = 900;
|
||||
@@ -100,70 +150,33 @@ func (f *FeedDetailAction) GetFeedDetail(ctx context.Context, feedID, xsecToken
|
||||
}
|
||||
};
|
||||
|
||||
// 点击所有"更多"按钮 - 使用多种策略确保不遗漏
|
||||
// 简化的点击"更多"按钮函数 - 只使用 .show-more 选择器
|
||||
const clickShowMoreButtons = () => {
|
||||
// 尝试多个可能的选择器
|
||||
const selectors = [
|
||||
'.show-more',
|
||||
'.show-more-btn',
|
||||
'[class*="show-more"]',
|
||||
'[class*="showMore"]',
|
||||
'button:has-text("更多")',
|
||||
'span:has-text("更多")',
|
||||
'div:has-text("更多")'
|
||||
];
|
||||
|
||||
const clickedElements = new Set();
|
||||
let clickedCount = 0;
|
||||
|
||||
selectors.forEach((selector) => {
|
||||
const elements = document.querySelectorAll('.show-more');
|
||||
|
||||
elements.forEach((el) => {
|
||||
try {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach((el) => {
|
||||
// 避免重复点击同一个元素
|
||||
if (clickedElements.has(el)) return;
|
||||
|
||||
// 检查元素文本是否包含"更多"或者是否有相关class
|
||||
const text = el.textContent || '';
|
||||
const className = el.className || '';
|
||||
const shouldClick = text.includes('更多') ||
|
||||
className.includes('show-more') ||
|
||||
className.includes('showMore');
|
||||
|
||||
if (!shouldClick) return;
|
||||
|
||||
// 检查元素是否可见(放宽条件,不要求完全在视口内)
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
const isVisible = (
|
||||
rect.height > 0 &&
|
||||
rect.width > 0 &&
|
||||
style.display !== 'none' &&
|
||||
style.visibility !== 'hidden' &&
|
||||
style.opacity !== '0' &&
|
||||
rect.top < window.innerHeight + 500 && // 允许元素在视口下方500px内
|
||||
rect.bottom > -500 // 允许元素在视口上方500px内
|
||||
);
|
||||
|
||||
if (isVisible) {
|
||||
try {
|
||||
// 尝试多种点击方式
|
||||
el.click();
|
||||
|
||||
// 如果是嵌套元素,也尝试点击父元素
|
||||
if (el.parentElement && el.parentElement.classList.contains('show-more')) {
|
||||
el.parentElement.click();
|
||||
}
|
||||
|
||||
clickedElements.add(el);
|
||||
clickedCount++;
|
||||
} catch (err) {
|
||||
console.debug('点击失败', err);
|
||||
}
|
||||
}
|
||||
});
|
||||
// 检查元素是否可见
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
const isVisible = (
|
||||
rect.height > 0 &&
|
||||
rect.width > 0 &&
|
||||
style.display !== 'none' &&
|
||||
style.visibility !== 'hidden' &&
|
||||
style.opacity !== '0' &&
|
||||
rect.top < window.innerHeight + 500 && // 允许元素在视口下方500px内
|
||||
rect.bottom > -500 // 允许元素在视口上方500px内
|
||||
);
|
||||
|
||||
if (isVisible) {
|
||||
el.click();
|
||||
clickedCount++;
|
||||
}
|
||||
} catch (err) {
|
||||
console.debug('选择器错误: ' + selector, err);
|
||||
console.debug('点击失败', err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -244,6 +257,7 @@ func (f *FeedDetailAction) GetFeedDetail(ctx context.Context, feedID, xsecToken
|
||||
dispatchWheel(root, applied);
|
||||
}
|
||||
};
|
||||
|
||||
return (async () => {
|
||||
let lastCount = 0;
|
||||
let stagnantChecks = 0;
|
||||
@@ -346,6 +360,7 @@ func (f *FeedDetailAction) GetFeedDetail(ctx context.Context, feedID, xsecToken
|
||||
}
|
||||
}
|
||||
|
||||
// === 提取笔记详情数据 ===
|
||||
result := page.MustEval(`() => {
|
||||
if (window.__INITIAL_STATE__ &&
|
||||
window.__INITIAL_STATE__.note &&
|
||||
|
||||
Reference in New Issue
Block a user