- 新增小红书前端模块(启动服务、内容分析、爆款创作、发布记录、技能卡片、拟人设置、AI 助手) - 新增 xhs-agent-desktop 独立桌面 Agent,对接 OMS 任务调度 - 登录页重构,整合微信登录与 OMS JWT 认证 - 订单、渠道、用户中心等页面与 API 增强 Co-authored-by: Cursor <cursoragent@cursor.com>
129 lines
4.0 KiB
Python
129 lines
4.0 KiB
Python
"""评论操作,对应 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
|