"""评论操作,对应 Go xiaohongshu/comment_feed.go。""" from __future__ import annotations import json import logging from .cdp import Page from .feed_detail import _check_page_accessible, navigate_to_feed_detail from .human import sleep_random from .selectors import ( COMMENT_INPUT_FIELD, COMMENT_INPUT_TRIGGER, COMMENT_SUBMIT_BUTTON, PARENT_COMMENT, REPLY_BUTTON, ) logger = logging.getLogger(__name__) def post_comment( page: Page, feed_id: str, xsec_token: str, content: str, *, on_progress=None, ) -> None: """发表评论到 Feed。""" url = navigate_to_feed_detail(page, feed_id, xsec_token) _check_page_accessible(page, url) if not page.has_element(COMMENT_INPUT_TRIGGER): raise RuntimeError("未找到评论输入框,该帖子可能不支持评论或网页端不可访问") page.click_element(COMMENT_INPUT_TRIGGER) sleep_random(400, 800) page.wait_for_element(COMMENT_INPUT_FIELD, timeout=5) page.input_content_editable(COMMENT_INPUT_FIELD, content) sleep_random(600, 1200) page.click_element(COMMENT_SUBMIT_BUTTON) sleep_random(800, 1500) logger.info("评论发送成功: feed=%s", feed_id) def reply_comment( page: Page, feed_id: str, xsec_token: str, content: str, comment_id: str = "", user_id: str = "", *, on_progress=None, ) -> None: """回复指定评论。""" if not comment_id and not user_id: raise ValueError("comment_id 和 user_id 至少提供一个") url = navigate_to_feed_detail(page, feed_id, xsec_token) _check_page_accessible(page, url) sleep_random(1500, 2500) from .feed_detail import _scroll_to_comments_area from .selectors import END_CONTAINER _scroll_to_comments_area(page) sleep_random(800, 1200) index = _find_target_comment_index(page, comment_id, user_id) if index < 0: for _ in range(24): page.dispatch_wheel_event(500) sleep_random(400, 700) index = _find_target_comment_index(page, comment_id, user_id) if index >= 0: break if page.get_element_text(END_CONTAINER): break if index < 0: raise RuntimeError("未找到目标评论,请确认 comment_id / user_id 是否正确") page.scroll_nth_element_into_view(PARENT_COMMENT, index) sleep_random(400, 800) page.evaluate( f"""() => {{ const parents = document.querySelectorAll({json.dumps(PARENT_COMMENT)}); const el = parents[{index}]; if (!el) throw new Error('评论元素不存在'); const btn = el.querySelector('.reply') || el.querySelector({json.dumps(REPLY_BUTTON)}); if (!btn) throw new Error('未找到回复按钮'); btn.click(); }}""" ) sleep_random(500, 900) page.wait_for_element(COMMENT_INPUT_FIELD, timeout=8) page.input_content_editable(COMMENT_INPUT_FIELD, content) sleep_random(600, 1200) page.click_element(COMMENT_SUBMIT_BUTTON) sleep_random(800, 1500) logger.info("回复评论成功: feed=%s", feed_id) def _find_target_comment_index(page: Page, comment_id: str, user_id: str) -> int: """在已渲染的 .parent-comment 中查找目标评论索引。""" cid = json.dumps(comment_id or "") uid = json.dumps(user_id or "") result = page.evaluate( f"""() => {{ const parents = document.querySelectorAll({json.dumps(PARENT_COMMENT)}); for (let i = 0; i < parents.length; i++) {{ const el = parents[i]; const id = el.id || el.getAttribute('data-comment-id') || el.getAttribute('data-id') || ''; if ({cid} && id && id.includes({cid})) return i; const userLink = el.querySelector('a[href*="/user/profile/"]'); if ({uid} && userLink && userLink.href.includes({uid})) return i; }} return -1; }}""" ) try: return int(result) except (TypeError, ValueError): return -1