from __future__ import annotations import json from typing import Any # 从 API/CLI 载荷中提取笔记列表时尝试的键(首页、搜索通用) _FEED_LIST_KEYS = ("feeds", "items", "list", "results", "notes", "records") def _normalize_profile_feed_item(item: dict[str, Any]) -> dict[str, Any]: """将 user-profile / my-account 的扁平 feed 转为列表行。""" if item.get("noteCard") or item.get("note_card") or item.get("note"): return _normalize_feed_item(item) note_card: dict[str, Any] = { "displayTitle": item.get("displayTitle") or item.get("display_title") or item.get("title"), "type": item.get("type"), "user": item.get("user") if isinstance(item.get("user"), dict) else {}, "interactInfo": item.get("interactInfo") if isinstance(item.get("interactInfo"), dict) else item.get("interact_info") if isinstance(item.get("interact_info"), dict) else {}, } if item.get("video"): note_card["video"] = item.get("video") return _normalize_feed_item( { "id": item.get("id") or item.get("feed_id"), "xsecToken": item.get("xsecToken") or item.get("xsec_token"), "modelType": item.get("modelType") or item.get("model_type") or "note", "noteCard": note_card, } ) def parse_user_profile_feeds(data: Any) -> list[dict[str, Any]]: """解析用户主页 / 我的账号接口返回的 feeds 数组。""" if data is None: return [] if isinstance(data, dict): feeds = data.get("feeds") if isinstance(feeds, list): return [ _normalize_profile_feed_item(x) for x in feeds if isinstance(x, dict) ] if isinstance(data, list): return [_normalize_profile_feed_item(x) for x in data if isinstance(x, dict)] return [] def parse_feeds_payload(data: Any) -> list[dict[str, Any]]: """将 Skills CLI 返回统一解析为 Feed 行列表(首页推荐、搜索内容共用)。""" if data is None: return [] if isinstance(data, str): text = data.strip() if not text: return [] try: data = json.loads(text) except json.JSONDecodeError: return [] if isinstance(data, list): return [_normalize_feed_item(x) for x in data if isinstance(x, dict)] if isinstance(data, dict): feeds = _extract_feeds_list(data) if not feeds: val = data.get("feeds") if isinstance(val, list): feeds = val return [_normalize_profile_feed_item(x) for x in feeds if isinstance(x, dict)] return [] def _extract_feeds_list(data: dict[str, Any]) -> list[Any]: """从 dict 载荷中取出笔记数组(兼容搜索/首页不同包裹层)。""" for key in _FEED_LIST_KEYS: val = data.get(key) if isinstance(val, list): return val inner = data.get("data") if isinstance(inner, dict): for key in _FEED_LIST_KEYS: val = inner.get(key) if isinstance(val, list): return val search = data.get("search") if isinstance(search, dict): for key in _FEED_LIST_KEYS: val = search.get(key) if isinstance(val, list): return val feeds_wrap = search.get("feeds") if isinstance(feeds_wrap, dict): val = feeds_wrap.get("value") or feeds_wrap.get("_value") if isinstance(val, list): return val return [] def _note_dict(item: dict[str, Any]) -> dict[str, Any]: """合并 noteCard / note_card / note(搜索与首页字段名不一致)。""" merged: dict[str, Any] = {} for key in ("noteCard", "note_card", "note"): block = item.get(key) if isinstance(block, dict): merged.update(block) return merged def _merge_interact(note: dict[str, Any], item: dict[str, Any]) -> dict[str, Any]: """合并各层 interactInfo(noteCard 与 CLI to_dict 顶层可能各有一部分)。""" merged: dict[str, Any] = {} for src in ( note.get("interactInfo"), note.get("interact_info"), item.get("interactInfo"), item.get("interact_info"), item.get("interact"), ): if isinstance(src, dict): merged.update(src) return merged def _interact_count(interact: dict[str, Any], *keys: str) -> str: for key in keys: val = interact.get(key) if val is None: continue text = str(val).strip() if text: return text return "0" def _comment_count(note: dict[str, Any], item: dict[str, Any], interact: dict[str, Any]) -> str: count = _interact_count( interact, "commentCount", "comment_count", "commentsCount", "comments_count", ) if count != "0": return count count = _interact_count( note, "commentCount", "comment_count", "commentsCount", "comments_count", ) if count != "0": return count return _interact_count( item, "commentCount", "comment_count", "commentsCount", "comments_count", ) def _liked_count(note: dict[str, Any], item: dict[str, Any], interact: dict[str, Any]) -> str: count = _interact_count(interact, "likedCount", "liked_count", "liked") if count != "0": return count count = _interact_count(note, "likedCount", "liked_count", "liked") if count != "0": return count return _interact_count(item, "likedCount", "liked_count", "liked") NOTE_TYPE_VIDEO = "视频" NOTE_TYPE_IMAGE = "图文" def format_note_type_label(raw: Any) -> str: """将笔记类型转为中文:图文 / 视频。""" text = str(raw or "").strip() if not text: return "" lower = text.lower() if lower in ("video", "videonote", "video_note", "videonotev2"): return NOTE_TYPE_VIDEO if lower in ("normal", "image", "pic", "photo", "图文"): return NOTE_TYPE_IMAGE # modelType 常为 note,不能当作图文/视频 if lower == "note": return "" if "video" in lower: return NOTE_TYPE_VIDEO if lower in ("图文",): return NOTE_TYPE_IMAGE return "" def _has_video_payload(note: dict[str, Any], item: dict[str, Any]) -> bool: if item.get("video") or note.get("video"): return True for key in ("videoDuration", "video_duration", "videoDurationText"): if note.get(key) or item.get(key): return True cover = note.get("cover") or note.get("imageList") or note.get("image_list") if isinstance(cover, dict) and cover.get("video"): return True if isinstance(cover, list): for img in cover: if isinstance(img, dict) and ( img.get("video") or str(img.get("type") or "").lower() == "video" ): return True return False def _resolve_note_type(note: dict[str, Any], item: dict[str, Any]) -> str: if _has_video_payload(note, item): return NOTE_TYPE_VIDEO # noteCard.type 才是列表项图文/视频的主要依据(优先于 modelType) candidates = ( note.get("type"), note.get("noteType"), note.get("note_type"), item.get("type"), item.get("noteType"), item.get("note_type"), item.get("modelType"), item.get("model_type"), note.get("modelType"), note.get("model_type"), ) for raw in candidates: label = format_note_type_label(raw) if label in (NOTE_TYPE_VIDEO, NOTE_TYPE_IMAGE): return label return "—" def _normalize_feed_item(item: dict[str, Any]) -> dict[str, Any]: note = _note_dict(item) user = note.get("user") if isinstance(note.get("user"), dict) else {} interact = _merge_interact(note, item) title = ( note.get("displayTitle") or note.get("display_title") or note.get("title") or item.get("displayTitle") or item.get("display_title") or item.get("title") or "(无标题)" ) if not user: user = item.get("user") if isinstance(item.get("user"), dict) else {} author = ( user.get("nickname") or user.get("nickName") or user.get("nick_name") or user.get("name") or item.get("nickname") or item.get("nickName") or item.get("author") or "—" ) return { "feed_id": str(item.get("id") or item.get("feed_id") or ""), "xsec_token": str(item.get("xsecToken") or item.get("xsec_token") or ""), "title": str(title), "author": str(author), "liked": _liked_count(note, item, interact), "comments": _comment_count(note, item, interact), "collected": _interact_count( interact, "collectedCount", "collected_count", ) or _interact_count(note, "collectedCount", "collected_count") or _interact_count(item, "collectedCount", "collected_count"), "note_type": _resolve_note_type(note, item), }