- 新增小红书前端模块(启动服务、内容分析、爆款创作、发布记录、技能卡片、拟人设置、AI 助手) - 新增 xhs-agent-desktop 独立桌面 Agent,对接 OMS 任务调度 - 登录页重构,整合微信登录与 OMS JWT 认证 - 订单、渠道、用户中心等页面与 API 增强 Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
3.4 KiB
Python
105 lines
3.4 KiB
Python
"""首页 Feed 列表,对应 Go xiaohongshu/feeds.go。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from .browse_feeds import human_browse_feed_list
|
|
from .browse_plan_session import BrowsePlanSession
|
|
from .cdp import Page
|
|
from .collect_buffer import is_cancelled, persist_collected, raise_if_cancelled
|
|
from .errors import BrowseCancelledError, NoFeedsError
|
|
from .extract_feeds_state import extract_feeds_list, merge_feed_dicts
|
|
from .human import _sleep_interruptible as sleep_interruptible_sec
|
|
from .types import Feed
|
|
from .urls import HOME_URL
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def list_feeds(
|
|
page: Page,
|
|
*,
|
|
scroll_speed: str = "normal",
|
|
on_progress=None,
|
|
) -> tuple[list[Feed], bool, dict]:
|
|
"""获取首页 Feed 列表(按拟人计划滑动/点开,滚动过程中多次采集)。"""
|
|
session = BrowsePlanSession("home")
|
|
|
|
def progress(msg: str) -> None:
|
|
logger.info(msg)
|
|
if on_progress:
|
|
on_progress(msg)
|
|
|
|
collected: dict[str, dict] = {}
|
|
|
|
def absorb(label: str) -> int:
|
|
items = extract_feeds_list(page)
|
|
added = merge_feed_dicts(collected, items)
|
|
if added:
|
|
logger.info("%s: 新增 %d 条,累计 %d 条", label, added, len(collected))
|
|
persist_collected(collected)
|
|
raise_if_cancelled()
|
|
return added
|
|
|
|
meta = {
|
|
"browse_scroll_target": session.scroll_target,
|
|
"browse_click_target": session.click_target,
|
|
"browse_targets": session.targets.to_dict(),
|
|
"collect_elapsed_sec": 0.0,
|
|
"plan_completed": False,
|
|
}
|
|
|
|
try:
|
|
progress(session.start_message())
|
|
progress("打开首页并加载推荐…")
|
|
page.navigate(HOME_URL)
|
|
page.wait_for_load()
|
|
page.wait_dom_stable()
|
|
list_url = page.evaluate("window.location.href") or HOME_URL
|
|
progress("打开首页完成")
|
|
absorb("首页初次加载")
|
|
|
|
def on_scroll_collect() -> None:
|
|
absorb("滚动中")
|
|
|
|
human_browse_feed_list(
|
|
page,
|
|
list_url,
|
|
scroll_speed=scroll_speed,
|
|
browse_mode="list",
|
|
on_progress=on_progress,
|
|
after_scroll=on_scroll_collect,
|
|
run_targets=session.targets,
|
|
)
|
|
if not is_cancelled():
|
|
parse_sec = session.targets.parse_sec
|
|
if parse_sec > 0:
|
|
progress(
|
|
f"模拟浏览:提取并解析列表数据(约 {parse_sec:.1f} 秒)…"
|
|
)
|
|
sleep_interruptible_sec(parse_sec)
|
|
progress("模拟浏览:提取并解析列表数据…")
|
|
absorb("滚动结束后")
|
|
progress("模拟浏览:解析完成")
|
|
|
|
if not collected:
|
|
raise NoFeedsError()
|
|
|
|
meta["plan_completed"] = True
|
|
meta["collect_elapsed_sec"] = round(session.elapsed_sec(), 1)
|
|
progress(session.summary(len(collected), plan_completed=True))
|
|
return [Feed.from_dict(f) for f in collected.values()], False, meta
|
|
except BrowseCancelledError:
|
|
meta["collect_elapsed_sec"] = round(session.elapsed_sec(), 1)
|
|
if collected:
|
|
progress(
|
|
session.summary(
|
|
len(collected),
|
|
plan_completed=False,
|
|
user_cancelled=True,
|
|
)
|
|
)
|
|
return [Feed.from_dict(f) for f in collected.values()], True, meta
|
|
raise
|