From b34d80c092a391b3bf8aabc31da7647f70174561 Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Fri, 2 Jan 2026 21:43:02 +0800 Subject: [PATCH 01/42] =?UTF-8?q?=E4=BF=AE=E6=94=B9extra=E7=9A=84=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E5=8F=82=E6=95=B0=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加BV CV LIVE房间号等信息展示,extra扩展字段 --- .../parsers/bilibili/__init__.py | 325 ++++++++++++------ 1 file changed, 214 insertions(+), 111 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index b3457717..9580a08f 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -1,7 +1,7 @@ import json import asyncio from re import Match -from typing import ClassVar +from typing import ClassVar, Any from collections.abc import AsyncGenerator from msgspec import convert @@ -27,19 +27,30 @@ # 选择客户端 select_client("curl_cffi") # 模拟浏览器,第二参数数值参考 curl_cffi 文档 -# https://curl-cffi.readthedocs.io/en/latest/impersonate.html request_settings.set("impersonate", "chrome131") class BilibiliParser(BaseParser): # 平台信息 - platform: ClassVar[Platform] = Platform(name=PlatformEnum.BILIBILI, display_name="哔哩哔哩") + platform: ClassVar[Platform] = Platform( + name=PlatformEnum.BILIBILI, + display_name="哔哩哔哩", + ) def __init__(self): self.headers = HEADERS.copy() self._credential: Credential | None = None self._cookies_file = pconfig.config_dir / "bilibili_cookies.json" + # --- 辅助方法:格式化数字 --- + def _format_stat(self, num: int | None) -> str: + """将数字格式化为 1.2万 的形式""" + if num is None: + return "0" + if num >= 10000: + return f"{num / 10000:.1f}万" + return str(num) + @handle("b23.tv", r"b23\.tv/[A-Za-z\d\._?%&+\-=/#]+") @handle("bili2233", r"bili2233\.cn/[A-Za-z\d\._?%&+\-=/#]+") async def _parse_short_link(self, searched: Match[str]): @@ -88,7 +99,7 @@ async def _parse_favlist(self, searched: Match[str]): async def _parse_read(self, searched: Match[str]): """解析专栏信息""" read_id = int(searched.group("read_id")) - return await self.parse_read_with_opus(read_id) + return await self.parse_read(read_id) @handle("/opus/", r"bilibili\.com/opus/(?P\d+)") async def _parse_opus(self, searched: Match[str]): @@ -96,6 +107,7 @@ async def _parse_opus(self, searched: Match[str]): opus_id = int(searched.group("opus_id")) return await self.parse_opus(opus_id) + # --- 修改 parse_video --- async def parse_video( self, *, @@ -103,39 +115,59 @@ async def parse_video( avid: int | None = None, page_num: int = 1, ): - """解析视频信息 - - Args: - bvid (str | None): bvid - avid (int | None): avid - page_num (int): 页码 - """ - + """解析视频信息""" from .video import VideoInfo, AIConclusion video = await self._get_video(bvid=bvid, avid=avid) - # 转换为 msgspec struct - video_info = convert(await video.get_info(), VideoInfo) + info_data = await video.get_info() + video_info = convert(info_data, VideoInfo) + + # 1. 提取统计数据 + stats = {} + try: + if video_info.stat: + stats = { + "play": self._format_stat(video_info.stat.view), + "danmaku": self._format_stat(video_info.stat.danmaku), + "like": self._format_stat(video_info.stat.like), + "coin": self._format_stat(video_info.stat.coin), + "favorite": self._format_stat(video_info.stat.favorite), + "share": self._format_stat(video_info.stat.share), + "reply": self._format_stat(video_info.stat.reply), + } + logger.debug(f"[BiliParser] 视频统计数据: {stats}") + except Exception as e: + logger.warning(f"[BiliParser] 统计数据提取异常: {e}") + # 获取简介 text = f"简介: {video_info.desc}" if video_info.desc else None - # up + + # --- [修复] 使用位置参数调用 create_author --- author = self.create_author(video_info.owner.name, video_info.owner.face) - # 处理分 p + + # 分P信息 page_info = video_info.extract_info_with_page(page_num) - # 获取 AI 总结 + # AI 总结 + ai_summary = None if self._credential: - cid = await video.get_cid(page_info.index) - ai_conclusion = await video.get_ai_conclusion(cid) - ai_conclusion = convert(ai_conclusion, AIConclusion) - ai_summary = ai_conclusion.summary + try: + cid = await video.get_cid(page_info.index) + try: + ai_res = await video.get_ai_conclusion(cid) + ai_conclusion = convert(ai_res, AIConclusion) + ai_summary = ai_conclusion.summary + except Exception: + ai_summary = None + except Exception as e: + logger.warning(f"[BiliParser] AI总结获取失败: {e}") else: - ai_summary: str = "哔哩哔哩 cookie 未配置或失效, 无法使用 AI 总结" + ai_summary = "Cookie 未配置,无法获取 AI 总结" url = f"https://bilibili.com/{video_info.bvid}" url += f"?p={page_info.index + 1}" if page_info.index > 0 else "" - # 视频下载 task + # 下载任务 async def download_video(): output_path = pconfig.cache_dir / f"{video_info.bvid}-{page_num}.mp4" if output_path.exists(): @@ -156,87 +188,104 @@ async def download_video(): page_info.cover, page_info.duration, ) - - return self.result( + logger.debug(f"video_info: {video_info}") + # 【修改点】构造 extra_data + extra_data = { + "info": ai_summary, + "stats": stats, + "type": "video", + "type_tag": "视频", + "type_icon": "fa-circle-play", + "author_id": str(video_info.owner.mid), # 强制通过 extra 传递 UID + "content_id": video_info.bvid, # 传递 BV 号 + } + + # 创建结果 + res = self.result( url=url, title=page_info.title, timestamp=page_info.timestamp, text=text, author=author, contents=[video_content], - extra={"info": ai_summary}, ) + + # 强制注入 extra + res.extra = extra_data + logger.info(extra_data) + return res + # --- 修改 parse_dynamic --- async def parse_dynamic(self, dynamic_id: int): - """解析动态信息 - - Args: - url (str): 动态链接 - """ from bilibili_api.dynamic import Dynamic - - from .dynamic import DynamicData + from .dynamic import DynamicItem dynamic = Dynamic(dynamic_id, await self.credential) - dynamic_info = convert(await dynamic.get_info(), DynamicData).item - + dynamic_data = convert(await dynamic.get_info(), DynamicItem) + dynamic_info = dynamic_data.item + + # 使用位置参数 author = self.create_author(dynamic_info.name, dynamic_info.avatar) - # 下载图片 + # 提取动态统计数据 + stats = {} + try: + if dynamic_info.modules.module_stat: + m_stat = dynamic_info.modules.module_stat + stats = { + "like": self._format_stat(m_stat.get("like", {}).get("count", 0)), + "reply": self._format_stat(m_stat.get("comment", {}).get("count", 0)), + "share": self._format_stat(m_stat.get("forward", {}).get("count", 0)), + } + except Exception: + pass + contents: list[MediaContent] = [] for image_url in dynamic_info.image_urls: img_task = DOWNLOADER.download_img(image_url, ext_headers=self.headers) contents.append(ImageContent(img_task)) - - return self.result( - title=dynamic_info.title, + logger.debug(f"dynamic_info: {dynamic_info}") + extra_data = { + "stats": stats, + "type": "dynamic", + "type_tag": "动态", + "type_icon": "fa-quote-left", + "author_id": str(dynamic_info.modules.module_author.mid), + "content_id": str(dynamic_id), # 传递动态 ID + } + + res = self.result( + title=dynamic_info.title or "B站动态", text=dynamic_info.text, timestamp=dynamic_info.timestamp, author=author, contents=contents, ) + res.extra = extra_data + return res async def parse_opus(self, opus_id: int): - """解析图文动态信息 - - Args: - opus_id (int): 图文动态 id - """ opus = Opus(opus_id, await self.credential) return await self._parse_opus_obj(opus) - async def parse_read_with_opus(self, read_id: int): - """解析专栏信息, 使用 Opus 接口 - - Args: - read_id (int): 专栏 id - """ + async def parse_read_old(self, read_id: int): from bilibili_api.article import Article - article = Article(read_id) return await self._parse_opus_obj(await article.turn_to_opus()) async def _parse_opus_obj(self, bili_opus: Opus): - """解析图文动态信息 - - Args: - opus_id (int): 图文动态 id - - Returns: - ParseResult: 解析结果 - """ - from .opus import OpusItem, TextNode, ImageNode opus_info = await bili_opus.get_info() if not isinstance(opus_info, dict): raise ParseException("获取图文动态信息失败") - # 转换为结构体 + opus_data = convert(opus_info, OpusItem) logger.debug(f"opus_data: {opus_data}") + + # 使用位置参数解包 author = self.create_author(*opus_data.name_avatar) - # 按顺序处理图文内容(参考 parse_read 的逻辑) contents: list[MediaContent] = [] current_text = "" @@ -247,25 +296,72 @@ async def _parse_opus_obj(self, bili_opus: Opus): elif isinstance(node, TextNode): current_text += node.text - return self.result( + res = self.result( title=opus_data.title, author=author, timestamp=opus_data.timestamp, contents=contents, text=current_text.strip(), ) + return res - async def parse_live(self, room_id: int): - """解析直播信息 + # --- 修改 parse_read (专栏) --- + async def parse_read(self, read_id: int): + from bilibili_api.article import Article + from .article import TextNode, ImageNode, ArticleInfo + + ar = Article(read_id) + await ar.fetch_content() + data = ar.json() + article_info = convert(data, ArticleInfo) + + stats = {} + try: + if article_info.stats: + stats = { + "play": self._format_stat(article_info.stats.view), + "like": self._format_stat(article_info.stats.like), + "reply": self._format_stat(article_info.stats.reply), + "favorite": self._format_stat(article_info.stats.favorite), + "share": self._format_stat(article_info.stats.share), + "coin": self._format_stat(article_info.stats.coin), + } + except Exception: + pass - Args: - room_id (int): 直播 id + contents: list[MediaContent] = [] + current_text = "" + for child in article_info.gen_text_img(): + if isinstance(child, ImageNode): + contents.append(self.create_graphics_content(child.url, current_text.strip(), child.alt)) + current_text = "" + elif isinstance(child, TextNode): + current_text += child.text + + # 使用位置参数解包 + author = self.create_author(*article_info.author_info) + + extra_data = { + "stats": stats, + "type": "article", + "type_tag": "专栏", + "type_icon": "fa-newspaper", + "author_id": str(article_info.meta.author.mid), + "content_id": f"CV{read_id}", # 传递 cv 号 + } + logger.debug(f"article_info: {article_info}") + res = self.result( + title=article_info.title, + timestamp=article_info.timestamp, + text=current_text.strip(), + author=author, + contents=contents, + ) + res.extra = extra_data + return res - Returns: - ParseResult: 解析结果 - """ + async def parse_live(self, room_id: int): from bilibili_api.live import LiveRoom - from .live import RoomData room = LiveRoom(room_display_id=room_id, credential=await self.credential) @@ -273,41 +369,70 @@ async def parse_live(self, room_id: int): room_data = convert(info_dict, RoomData) contents: list[MediaContent] = [] - # 下载封面 if cover := room_data.cover: cover_task = DOWNLOADER.download_img(cover, ext_headers=self.headers) contents.append(ImageContent(cover_task)) - # 下载关键帧 if keyframe := room_data.keyframe: keyframe_task = DOWNLOADER.download_img(keyframe, ext_headers=self.headers) contents.append(ImageContent(keyframe_task)) + # 位置参数 author = self.create_author(room_data.name, room_data.avatar) url = f"https://www.bilibili.com/blackboard/live/live-activity-player.html?enterTheRoom=0&cid={room_id}" - return self.result( + logger.debug(f"room_data: {room_data}") + ''' + room_data: RoomData( + room_info=RoomInfo( + title='【榜金】你的好友正在炫耀大金', + cover='https://i0.hdslb.com/bfs/live/new_room_cover/921dc312abf6e0d3f27f9b29c01f89cc1441ceaa.jpg', + keyframe='https://i0.hdslb.com/bfs/live-key-frame/keyframe01022106001874910174skt0t2.jpg', + tags='逃出惊魂夜,蛋仔派对,惊魂寻宝队', + area_name='蛋仔派对', + parent_area_name='手游' + ), + anchor_info=AnchorInfo( + base_info=BaseInfo( + uname='粒粒强且自知', + face='https://i2.hdslb.com/bfs/face/596cce2ca8bfed7fb2aafaec262f8d4c5d03e7b5.jpg', + gender='女' + ), + live_info=LiveInfo( + level=21, + level_color=10512625, + score=1049292 + ) + ) + ) + ''' + extra_data = { + "type": "live", + "type_tag": f"直播·{room_data.parent_area_name}", + "type_icon": "fa-tower-broadcast", + "content_id": f"ROOM{room_id}", # 传递房间号 + "tags": str(room_data.room_info.tags), + "live_info": { + "level": str(room_data.anchor_info.live_info.level), + "level_color": str(room_data.anchor_info.live_info.level_color), + "score": str(room_data.anchor_info.live_info.score) + } + } + + res = self.result( url=url, title=room_data.title, text=room_data.detail, contents=contents, author=author, ) + res.extra = extra_data + return res async def parse_favlist(self, fav_id: int): - """解析收藏夹信息 - - Args: - fav_id (int): 收藏夹 id - - Returns: - list[GraphicsContent]: 图文内容列表 - """ from bilibili_api.favorite_list import get_video_favorite_list_content - from .favlist import FavData - # 只会取一页,20 个 fav_dict = await get_video_favorite_list_content(fav_id) if fav_dict["medias"] is None: @@ -315,20 +440,18 @@ async def parse_favlist(self, fav_id: int): favdata = convert(fav_dict, FavData) + # 位置参数 + author = self.create_author(favdata.info.upper.name, favdata.info.upper.face) + return self.result( title=favdata.title, timestamp=favdata.timestamp, - author=self.create_author(favdata.info.upper.name, favdata.info.upper.face), + author=author, contents=[self.create_graphics_content(fav.cover, fav.desc) for fav in favdata.medias], ) + # ... 后面的辅助方法保持不变 (_get_video, extract_download_urls, _save_credential 等) ... async def _get_video(self, *, bvid: str | None = None, avid: int | None = None) -> Video: - """解析视频信息 - - Args: - bvid (str | None): bvid - avid (int | None): avid - """ if avid: return Video(aid=avid, credential=await self.credential) elif bvid: @@ -344,14 +467,6 @@ async def extract_download_urls( avid: int | None = None, page_index: int = 0, ) -> tuple[str, str | None]: - """解析视频下载链接 - - Args: - bvid (str | None): bvid - avid (int | None): avid - page_index (int): 页索引 = 页码 - 1 - """ - from bilibili_api.video import ( AudioStreamDownloadURL, VideoStreamDownloadURL, @@ -361,7 +476,6 @@ async def extract_download_urls( if video is None: video = await self._get_video(bvid=bvid, avid=avid) - # 获取下载数据 download_url_data = await video.get_download_url(page_index=page_index) detecter = VideoDownloadURLDataDetecter(download_url_data) streams = detecter.detect_best_streams( @@ -382,31 +496,23 @@ async def extract_download_urls( return video_stream.url, audio_stream.url def _save_credential(self): - """存储哔哩哔哩登录凭证""" if self._credential is None: return - self._cookies_file.write_text(json.dumps(self._credential.get_cookies())) def _load_credential(self): - """从文件加载哔哩哔哩登录凭证""" if not self._cookies_file.exists(): return - self._credential = Credential.from_cookies(json.loads(self._cookies_file.read_text())) async def login_with_qrcode(self) -> bytes: - """通过二维码登录获取哔哩哔哩登录凭证""" self._qr_login = QrCodeLogin() await self._qr_login.generate_qrcode() - qr_pic = self._qr_login.get_qrcode_picture() return qr_pic.content async def check_qr_state(self) -> AsyncGenerator[str]: - """检查二维码登录状态""" scan_tip_pending = True - for _ in range(30): state = await self._qr_login.check_state() match state: @@ -427,7 +533,6 @@ async def check_qr_state(self) -> AsyncGenerator[str]: yield "二维码登录超时, 请重新生成" async def _init_credential(self): - """初始化哔哩哔哩登录凭证""" if pconfig.bili_ck is None: self._load_credential() return @@ -443,8 +548,6 @@ async def _init_credential(self): @property async def credential(self) -> Credential | None: - """哔哩哔哩登录凭证""" - if self._credential is None: await self._init_credential() return self._credential From 085be2ad588edc4aaff027beab9bdce38f59eedd Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Fri, 2 Jan 2026 21:44:20 +0800 Subject: [PATCH 02/42] Refactor render_image method and clean up docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 额外信息字段支持 --- .../renders/htmlrender.py | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/nonebot_plugin_parser/renders/htmlrender.py b/src/nonebot_plugin_parser/renders/htmlrender.py index bcd1519c..08def827 100644 --- a/src/nonebot_plugin_parser/renders/htmlrender.py +++ b/src/nonebot_plugin_parser/renders/htmlrender.py @@ -15,14 +15,7 @@ class HtmlRenderer(ImageRenderer): @override async def render_image(self, result: ParseResult) -> bytes: - """使用 HTML 绘制通用社交媒体帖子卡片 - - Args: - result: 解析结果 - - Returns: - PNG 图片的字节数据 - """ + """使用 HTML 绘制通用社交媒体帖子卡片""" # 准备模板数据 template_data = await self._resolve_parse_result(result) @@ -32,18 +25,20 @@ async def render_image(self, result: ParseResult) -> bytes: template_name="card.html.jinja", templates={"result": template_data}, pages={ - "viewport": {"width": 800, "height": 100}, # 高度会自动调整 + "viewport": {"width": 800, "height": 100}, "base_url": f"file://{self.templates_dir}", }, ) async def _resolve_parse_result(self, result: ParseResult) -> dict[str, Any]: - """解析 ParseResult 为模板可用的字典数据,并处理异步资源路径""" + """解析 ParseResult 为模板可用的字典数据""" + data: dict[str, Any] = { "title": result.title, "text": result.text, "formartted_datetime": result.formartted_datetime, "extra_info": result.extra_info, + "extra": result.extra } if result.platform: @@ -51,32 +46,36 @@ async def _resolve_parse_result(self, result: ParseResult) -> dict[str, Any]: "display_name": result.platform.display_name, "name": result.platform.name, } - # 尝试获取平台 logo - logo_path = Path(__file__).parent / "resources" / f"{result.platform.name}.png" - if logo_path.exists(): - data["platform"]["logo_path"] = logo_path.as_uri() + if hasattr(result.platform, "logo_path") and result.platform.logo_path: + data["platform"]["logo_path"] = result.platform.logo_path + else: + # 回退到本地资源查找 + logo_path = Path(__file__).parent / "resources" / f"{result.platform.name}.png" + if logo_path.exists(): + data["platform"]["logo_path"] = logo_path.as_uri() - # 处理作者信息 if result.author: avatar_path = await result.author.get_avatar_path() + author_id = getattr(result.author, "id", None) + if not author_id and result.extra: + author_id = result.extra.get("author_id") + data["author"] = { "name": result.author.name, + "id": author_id, # 传递 UID "avatar_path": avatar_path.as_uri() if avatar_path else None, } - # 处理封面 cover_path = await result.cover_path if cover_path: data["cover_path"] = cover_path.as_uri() - # 处理图片内容 img_contents = [] for img in result.img_contents: path = await img.get_path() img_contents.append({"path": path.as_uri()}) data["img_contents"] = img_contents - # 处理图文内容 graphics_contents = [] for graphics in result.graphics_contents: path = await graphics.get_path() @@ -89,7 +88,6 @@ async def _resolve_parse_result(self, result: ParseResult) -> dict[str, Any]: ) data["graphics_contents"] = graphics_contents - # 处理转发内容 if result.repost: data["repost"] = await self._resolve_parse_result(result.repost) From 3202b0db1a1cbffe8b378854631b02cd0220a70e Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:00:47 +0800 Subject: [PATCH 03/42] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AF=B9=E9=A2=9D?= =?UTF-8?q?=E5=A4=96=E6=A8=A1=E6=9D=BF=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 同文件夹平台名模板自动使用,不存在回退默认 --- src/nonebot_plugin_parser/renders/htmlrender.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/nonebot_plugin_parser/renders/htmlrender.py b/src/nonebot_plugin_parser/renders/htmlrender.py index 08def827..30547fb1 100644 --- a/src/nonebot_plugin_parser/renders/htmlrender.py +++ b/src/nonebot_plugin_parser/renders/htmlrender.py @@ -19,10 +19,20 @@ async def render_image(self, result: ParseResult) -> bytes: # 准备模板数据 template_data = await self._resolve_parse_result(result) + #处理模板针对 + template_name = "card.html.jinja" + if result.platform: + #添加存在性验证 + file_name = f"{str(result.platform.name).lower()}.html.jinja" + if (self.templates_dir / file_name).exists(): + template_name = file_name + else: + logger.debug(f"不存在对应平台模板,忽略回退") + # 渲染图片 return await template_to_pic( template_path=str(self.templates_dir), - template_name="card.html.jinja", + template_name=template_name, templates={"result": template_data}, pages={ "viewport": {"width": 800, "height": 100}, From f349fc13dbf2ed6d3ca3ec05c0f98d7bf6219851 Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:01:38 +0800 Subject: [PATCH 04/42] Fix type_tag assignment in Bilibili parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 type_tag --- src/nonebot_plugin_parser/parsers/bilibili/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index 9580a08f..a9ada394 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -408,7 +408,7 @@ async def parse_live(self, room_id: int): ''' extra_data = { "type": "live", - "type_tag": f"直播·{room_data.parent_area_name}", + "type_tag": f"直播·{room_data.room_info.parent_area_name}", "type_icon": "fa-tower-broadcast", "content_id": f"ROOM{room_id}", # 传递房间号 "tags": str(room_data.room_info.tags), From 34f235ab12978fe9d1f981f55f4190998dde7997 Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:04:07 +0800 Subject: [PATCH 05/42] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=93=94=E7=AB=99?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E5=B9=B3=E5=8F=B0HTML=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E5=8D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../renders/templates/bilibili.html.jinja | 602 ++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 src/nonebot_plugin_parser/renders/templates/bilibili.html.jinja diff --git a/src/nonebot_plugin_parser/renders/templates/bilibili.html.jinja b/src/nonebot_plugin_parser/renders/templates/bilibili.html.jinja new file mode 100644 index 00000000..cbacf4ce --- /dev/null +++ b/src/nonebot_plugin_parser/renders/templates/bilibili.html.jinja @@ -0,0 +1,602 @@ + + + + + + Bilibili Card + + + + + + {# === 宏定义:渲染卡片 === #} + {% macro render_card(result, is_repost=False) %} + + {# 提取 Extra 数据 (由 Parser 修改后注入) #} + {# 获取变量 #} + {% set extra = result.extra if result.extra else {} %} + {% set platform = result.platform if result.platform else {} %} + {# 获取 Content ID #} + {% set content_id = extra.content_id %} + {% set stats = extra.stats if extra.stats else {} %} + {% set type_tag = extra.type_tag if extra.type_tag else "B站" %} + {% set type_icon = extra.type_icon if extra.type_icon else "fa-brands fa-bilibili" %} + {% set type = extra.type if extra.type else "video" %} + +
+ {# 仅主卡片显示顶部彩条 #} + {% if not is_repost %} +
+ {% endif %} + + {# --- 封面区域 --- #} + {# 优先使用提取到的封面,如果是 repost 且没有封面,尝试使用第一张图片 #} + {% set cover_url = result.cover_path %} + {% if not cover_url and result.img_contents and result.img_contents|length > 0 %} + {% set cover_url = result.img_contents[0].path %} + {% endif %} + + {% if cover_url %} +
+ Cover +
+ + {# 【修改点 1】只有 B 站才显示左侧的 类型标签 (视频/专栏等) #} + {# 因为其他平台可能没有 type_tag 这个字段或者不需要显示 #} + {% if platform.name == 'bilibili' and extra.type_tag %} +
+ + {{ extra.type_tag }} +
+ {% endif %} + + {# 右上角:平台标签 (一直显示) #} + {% if platform %} +
+ {% if result.platform.logo_path %} + + {% else %} + + {% endif %} +
+ {% endif %} + + {# 中间:播放按钮装饰 (仅视频/音频) #} + {% if type in ['video', 'live'] %} +
+ +
+ {% endif %} +
+ {% endif %} + +
+ {# --- 作者信息行 --- #} + {% if result.author %} +
+
+ + +
+

{{ result.author.name }}

+
+ {# 显示 UID #} + {% if extra.author_id %} + UID: {{ extra.author_id }} + {% endif %} + + {# 【修改点 2】显示内容 ID (BV/CV/RoomID) #} + {% if content_id %} + {{ content_id }} + {% endif %} + + {# 时间 #} + {% if result.formartted_datetime %} + {{ result.formartted_datetime }} + {% endif %} +
+
+
+ + {# 装饰性二维码图标 #} +
+ +
+
+ {% endif %} + + {# --- 统计数据网格 (仅主卡片显示) --- #} + {% if stats and not is_repost %} +
+ +
+
+
+
{{ stats.play or '-' }}
+
播放
+
+
+ +
+ +
+
{{ stats.like or '-' }}
+
点赞
+
+
+ +
+
+
+
{{ stats.reply or '-' }}
+
评论
+
+
+ +
+
+
+
{{ stats.favorite or '-' }}
+
收藏
+
+
+ +
+ +
+
{{ stats.share or '-' }}
+
分享
+
+
+ +
+
+ {% if stats.coin %} + + {% else %} + + {% endif %} +
+
+
{{ stats.coin or stats.danmaku or '-' }}
+
{{ '硬币' if stats.coin else '弹幕' }}
+
+
+
+ {% endif %} + + {# --- 主要内容 --- #} + {% if result.title %} +
{{ result.title }}
+ {% endif %} + + {# AI 总结 #} + {% if extra.info %} +
+

AI 内容总结

+

{{ extra.info }}

+
+ {% endif %} + + {# 正文描述 #} + {% if result.text %} +
{{ result.text }}
+ {% endif %} + + {# --- 图片网格 (除去封面) --- #} + {# 如果第一张图被用作封面了,这里可能需要排除它,但通常动态图多张都需要展示 #} + {# 这里逻辑:如果有图片内容,且不是视频(视频封面已在上面展示) #} + {% if result.img_contents %} + {% set imgs = result.img_contents %} + {# 如果是视频/专栏,可能封面就是第一张图,这里根据实际情况可以切片 imgs[1:],目前全显示 #} + + {% set count = imgs|length %} + {% if count > 0 %} +
+
内容预览
+ + {# 计算网格类名 #} + {% set grid_class = 'single' %} + {% if count == 2 %}{% set grid_class = 'double' %} + {% elif count == 4 %}{% set grid_class = 'quad' %} + {% elif count >= 3 %}{% set grid_class = 'nine' %} + {% endif %} + +
+ {# 限制最多显示9张 #} + {% for img in imgs[:9] %} +
+ + {% if loop.last and count > 9 %} +
+{{ count - 9 }}
+ {% endif %} +
+ {% endfor %} +
+
+ {% endif %} + {% endif %} + + {# --- 嵌套转发内容 --- #} + {% if result.repost %} +
+
+ 转发内容 +
+ {{ render_card(result.repost, is_repost=True) }} +
+ {% endif %} +
+ + {# --- 底部信息栏 --- #} + {% if not is_repost %} + + {% endif %} +
+ {% endmacro %} + + {# 执行宏渲染 #} + {{ render_card(result) }} + + + From 340d908fd81fc48a5b0f5cb830b1dd3a1613aa19 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 14:06:10 +0000 Subject: [PATCH 06/42] chore: auto fix by pre-commit hooks --- .../parsers/bilibili/__init__.py | 45 ++++++++++--------- .../renders/htmlrender.py | 12 ++--- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index a9ada394..aa5fe5e4 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -1,7 +1,7 @@ import json import asyncio from re import Match -from typing import ClassVar, Any +from typing import Any, ClassVar from collections.abc import AsyncGenerator from msgspec import convert @@ -33,7 +33,7 @@ class BilibiliParser(BaseParser): # 平台信息 platform: ClassVar[Platform] = Platform( - name=PlatformEnum.BILIBILI, + name=PlatformEnum.BILIBILI, display_name="哔哩哔哩", ) @@ -121,7 +121,7 @@ async def parse_video( video = await self._get_video(bvid=bvid, avid=avid) info_data = await video.get_info() video_info = convert(info_data, VideoInfo) - + # 1. 提取统计数据 stats = {} try: @@ -141,7 +141,7 @@ async def parse_video( # 获取简介 text = f"简介: {video_info.desc}" if video_info.desc else None - + # --- [修复] 使用位置参数调用 create_author --- author = self.create_author(video_info.owner.name, video_info.owner.face) @@ -196,8 +196,8 @@ async def download_video(): "type": "video", "type_tag": "视频", "type_icon": "fa-circle-play", - "author_id": str(video_info.owner.mid), # 强制通过 extra 传递 UID - "content_id": video_info.bvid, # 传递 BV 号 + "author_id": str(video_info.owner.mid), # 强制通过 extra 传递 UID + "content_id": video_info.bvid, # 传递 BV 号 } # 创建结果 @@ -209,7 +209,7 @@ async def download_video(): author=author, contents=[video_content], ) - + # 强制注入 extra res.extra = extra_data logger.info(extra_data) @@ -218,12 +218,13 @@ async def download_video(): # --- 修改 parse_dynamic --- async def parse_dynamic(self, dynamic_id: int): from bilibili_api.dynamic import Dynamic + from .dynamic import DynamicItem dynamic = Dynamic(dynamic_id, await self.credential) dynamic_data = convert(await dynamic.get_info(), DynamicItem) dynamic_info = dynamic_data.item - + # 使用位置参数 author = self.create_author(dynamic_info.name, dynamic_info.avatar) @@ -251,7 +252,7 @@ async def parse_dynamic(self, dynamic_id: int): "type_tag": "动态", "type_icon": "fa-quote-left", "author_id": str(dynamic_info.modules.module_author.mid), - "content_id": str(dynamic_id), # 传递动态 ID + "content_id": str(dynamic_id), # 传递动态 ID } res = self.result( @@ -270,6 +271,7 @@ async def parse_opus(self, opus_id: int): async def parse_read_old(self, read_id: int): from bilibili_api.article import Article + article = Article(read_id) return await self._parse_opus_obj(await article.turn_to_opus()) @@ -279,10 +281,10 @@ async def _parse_opus_obj(self, bili_opus: Opus): opus_info = await bili_opus.get_info() if not isinstance(opus_info, dict): raise ParseException("获取图文动态信息失败") - + opus_data = convert(opus_info, OpusItem) logger.debug(f"opus_data: {opus_data}") - + # 使用位置参数解包 author = self.create_author(*opus_data.name_avatar) @@ -308,13 +310,14 @@ async def _parse_opus_obj(self, bili_opus: Opus): # --- 修改 parse_read (专栏) --- async def parse_read(self, read_id: int): from bilibili_api.article import Article + from .article import TextNode, ImageNode, ArticleInfo ar = Article(read_id) await ar.fetch_content() data = ar.json() article_info = convert(data, ArticleInfo) - + stats = {} try: if article_info.stats: @@ -340,14 +343,14 @@ async def parse_read(self, read_id: int): # 使用位置参数解包 author = self.create_author(*article_info.author_info) - + extra_data = { "stats": stats, "type": "article", "type_tag": "专栏", "type_icon": "fa-newspaper", "author_id": str(article_info.meta.author.mid), - "content_id": f"CV{read_id}", # 传递 cv 号 + "content_id": f"CV{read_id}", # 传递 cv 号 } logger.debug(f"article_info: {article_info}") res = self.result( @@ -362,6 +365,7 @@ async def parse_read(self, read_id: int): async def parse_live(self, room_id: int): from bilibili_api.live import LiveRoom + from .live import RoomData room = LiveRoom(room_display_id=room_id, credential=await self.credential) @@ -382,7 +386,7 @@ async def parse_live(self, room_id: int): url = f"https://www.bilibili.com/blackboard/live/live-activity-player.html?enterTheRoom=0&cid={room_id}" logger.debug(f"room_data: {room_data}") - ''' + """ room_data: RoomData( room_info=RoomInfo( title='【榜金】你的好友正在炫耀大金', @@ -405,20 +409,20 @@ async def parse_live(self, room_id: int): ) ) ) - ''' + """ extra_data = { "type": "live", "type_tag": f"直播·{room_data.room_info.parent_area_name}", "type_icon": "fa-tower-broadcast", - "content_id": f"ROOM{room_id}", # 传递房间号 + "content_id": f"ROOM{room_id}", # 传递房间号 "tags": str(room_data.room_info.tags), "live_info": { "level": str(room_data.anchor_info.live_info.level), "level_color": str(room_data.anchor_info.live_info.level_color), - "score": str(room_data.anchor_info.live_info.score) - } + "score": str(room_data.anchor_info.live_info.score), + }, } - + res = self.result( url=url, title=room_data.title, @@ -431,6 +435,7 @@ async def parse_live(self, room_id: int): async def parse_favlist(self, fav_id: int): from bilibili_api.favorite_list import get_video_favorite_list_content + from .favlist import FavData fav_dict = await get_video_favorite_list_content(fav_id) diff --git a/src/nonebot_plugin_parser/renders/htmlrender.py b/src/nonebot_plugin_parser/renders/htmlrender.py index 30547fb1..d0a29e70 100644 --- a/src/nonebot_plugin_parser/renders/htmlrender.py +++ b/src/nonebot_plugin_parser/renders/htmlrender.py @@ -19,15 +19,15 @@ async def render_image(self, result: ParseResult) -> bytes: # 准备模板数据 template_data = await self._resolve_parse_result(result) - #处理模板针对 + # 处理模板针对 template_name = "card.html.jinja" if result.platform: - #添加存在性验证 + # 添加存在性验证 file_name = f"{str(result.platform.name).lower()}.html.jinja" if (self.templates_dir / file_name).exists(): template_name = file_name else: - logger.debug(f"不存在对应平台模板,忽略回退") + logger.debug("不存在对应平台模板,忽略回退") # 渲染图片 return await template_to_pic( @@ -42,13 +42,13 @@ async def render_image(self, result: ParseResult) -> bytes: async def _resolve_parse_result(self, result: ParseResult) -> dict[str, Any]: """解析 ParseResult 为模板可用的字典数据""" - + data: dict[str, Any] = { "title": result.title, "text": result.text, "formartted_datetime": result.formartted_datetime, "extra_info": result.extra_info, - "extra": result.extra + "extra": result.extra, } if result.platform: @@ -72,7 +72,7 @@ async def _resolve_parse_result(self, result: ParseResult) -> dict[str, Any]: data["author"] = { "name": result.author.name, - "id": author_id, # 传递 UID + "id": author_id, # 传递 UID "avatar_path": avatar_path.as_uri() if avatar_path else None, } From a648706c4c2d8ef5575ac894857d46224a75b346 Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:16:07 +0800 Subject: [PATCH 07/42] Update dynamic.py From a220adc1074553eade990e43645b312f11e1a4eb Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:17:02 +0800 Subject: [PATCH 08/42] Update import and conversion in parse_dynamic method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复dynamic导入问题 --- src/nonebot_plugin_parser/parsers/bilibili/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index aa5fe5e4..552073a1 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -219,10 +219,10 @@ async def download_video(): async def parse_dynamic(self, dynamic_id: int): from bilibili_api.dynamic import Dynamic - from .dynamic import DynamicItem + from .dynamic import DynamicData dynamic = Dynamic(dynamic_id, await self.credential) - dynamic_data = convert(await dynamic.get_info(), DynamicItem) + dynamic_data = convert(await dynamic.get_info(), DynamicData) dynamic_info = dynamic_data.item # 使用位置参数 From 6daab0c7bb88421a38172c728c87d8940e59678e Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:17:37 +0800 Subject: [PATCH 09/42] Update __init__.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除备用注释 --- .../parsers/bilibili/__init__.py | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index 552073a1..67ddd437 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -386,30 +386,6 @@ async def parse_live(self, room_id: int): url = f"https://www.bilibili.com/blackboard/live/live-activity-player.html?enterTheRoom=0&cid={room_id}" logger.debug(f"room_data: {room_data}") - """ - room_data: RoomData( - room_info=RoomInfo( - title='【榜金】你的好友正在炫耀大金', - cover='https://i0.hdslb.com/bfs/live/new_room_cover/921dc312abf6e0d3f27f9b29c01f89cc1441ceaa.jpg', - keyframe='https://i0.hdslb.com/bfs/live-key-frame/keyframe01022106001874910174skt0t2.jpg', - tags='逃出惊魂夜,蛋仔派对,惊魂寻宝队', - area_name='蛋仔派对', - parent_area_name='手游' - ), - anchor_info=AnchorInfo( - base_info=BaseInfo( - uname='粒粒强且自知', - face='https://i2.hdslb.com/bfs/face/596cce2ca8bfed7fb2aafaec262f8d4c5d03e7b5.jpg', - gender='女' - ), - live_info=LiveInfo( - level=21, - level_color=10512625, - score=1049292 - ) - ) - ) - """ extra_data = { "type": "live", "type_tag": f"直播·{room_data.room_info.parent_area_name}", From 296b3aac889cd2015ed35e5457145416f2d6d9ca Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:20:40 +0800 Subject: [PATCH 10/42] Update htmlrender.py --- src/nonebot_plugin_parser/renders/htmlrender.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/nonebot_plugin_parser/renders/htmlrender.py b/src/nonebot_plugin_parser/renders/htmlrender.py index d0a29e70..f6977a9e 100644 --- a/src/nonebot_plugin_parser/renders/htmlrender.py +++ b/src/nonebot_plugin_parser/renders/htmlrender.py @@ -26,8 +26,6 @@ async def render_image(self, result: ParseResult) -> bytes: file_name = f"{str(result.platform.name).lower()}.html.jinja" if (self.templates_dir / file_name).exists(): template_name = file_name - else: - logger.debug("不存在对应平台模板,忽略回退") # 渲染图片 return await template_to_pic( From 34076cebe99e8e570bfd986c5261b0060f4057fe Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:23:38 +0800 Subject: [PATCH 11/42] Update __init__.py --- src/nonebot_plugin_parser/parsers/bilibili/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index 67ddd437..b6c718ca 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -1,7 +1,7 @@ import json import asyncio from re import Match -from typing import Any, ClassVar +from typing import ClassVar from collections.abc import AsyncGenerator from msgspec import convert From 98cccbd6bab15d194f0cf87752923615f527af0a Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:26:25 +0800 Subject: [PATCH 12/42] Refactor platform logo path retrieval logic --- src/nonebot_plugin_parser/renders/htmlrender.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/nonebot_plugin_parser/renders/htmlrender.py b/src/nonebot_plugin_parser/renders/htmlrender.py index f6977a9e..26681442 100644 --- a/src/nonebot_plugin_parser/renders/htmlrender.py +++ b/src/nonebot_plugin_parser/renders/htmlrender.py @@ -54,13 +54,10 @@ async def _resolve_parse_result(self, result: ParseResult) -> dict[str, Any]: "display_name": result.platform.display_name, "name": result.platform.name, } - if hasattr(result.platform, "logo_path") and result.platform.logo_path: - data["platform"]["logo_path"] = result.platform.logo_path - else: - # 回退到本地资源查找 - logo_path = Path(__file__).parent / "resources" / f"{result.platform.name}.png" - if logo_path.exists(): - data["platform"]["logo_path"] = logo_path.as_uri() + # 尝试获取平台 logo + logo_path = Path(__file__).parent / "resources" / f"{result.platform.name}.png" + if logo_path.exists(): + data["platform"]["logo_path"] = logo_path.as_uri() if result.author: avatar_path = await result.author.get_avatar_path() From c1512d57471de75a29f9bbb68ceb7496dc2869f9 Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:40:32 +0800 Subject: [PATCH 13/42] Refactor BilibiliParser for improved functionality --- .../parsers/bilibili/__init__.py | 169 ++++++++++-------- 1 file changed, 92 insertions(+), 77 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index b6c718ca..844927e6 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -1,7 +1,7 @@ import json import asyncio from re import Match -from typing import ClassVar +from typing import ClassVar, Any from collections.abc import AsyncGenerator from msgspec import convert @@ -27,14 +27,15 @@ # 选择客户端 select_client("curl_cffi") # 模拟浏览器,第二参数数值参考 curl_cffi 文档 +# https://curl-cffi.readthedocs.io/en/latest/impersonate.html request_settings.set("impersonate", "chrome131") class BilibiliParser(BaseParser): # 平台信息 platform: ClassVar[Platform] = Platform( - name=PlatformEnum.BILIBILI, - display_name="哔哩哔哩", + name=PlatformEnum.BILIBILI, + display_name="哔哩哔哩" ) def __init__(self): @@ -98,6 +99,8 @@ async def _parse_favlist(self, searched: Match[str]): @handle("/read/", r"bilibili\.com/read/cv(?P\d+)") async def _parse_read(self, searched: Match[str]): """解析专栏信息""" + # 注意:这里改回了调用 parse_read 而不是 parse_read_with_opus + # 以保留你自定义的 stats 提取逻辑 read_id = int(searched.group("read_id")) return await self.parse_read(read_id) @@ -107,7 +110,6 @@ async def _parse_opus(self, searched: Match[str]): opus_id = int(searched.group("opus_id")) return await self.parse_opus(opus_id) - # --- 修改 parse_video --- async def parse_video( self, *, @@ -121,7 +123,7 @@ async def parse_video( video = await self._get_video(bvid=bvid, avid=avid) info_data = await video.get_info() video_info = convert(info_data, VideoInfo) - + # 1. 提取统计数据 stats = {} try: @@ -141,8 +143,8 @@ async def parse_video( # 获取简介 text = f"简介: {video_info.desc}" if video_info.desc else None - - # --- [修复] 使用位置参数调用 create_author --- + + # 使用位置参数调用 create_author author = self.create_author(video_info.owner.name, video_info.owner.face) # 分P信息 @@ -162,12 +164,12 @@ async def parse_video( except Exception as e: logger.warning(f"[BiliParser] AI总结获取失败: {e}") else: - ai_summary = "Cookie 未配置,无法获取 AI 总结" + ai_summary = "哔哩哔哩 cookie 未配置或失效, 无法使用 AI 总结" url = f"https://bilibili.com/{video_info.bvid}" url += f"?p={page_info.index + 1}" if page_info.index > 0 else "" - # 下载任务 + # 视频下载 task async def download_video(): output_path = pconfig.cache_dir / f"{video_info.bvid}-{page_num}.mp4" if output_path.exists(): @@ -188,16 +190,16 @@ async def download_video(): page_info.cover, page_info.duration, ) - logger.debug(f"video_info: {video_info}") - # 【修改点】构造 extra_data + + # 构造 extra_data extra_data = { "info": ai_summary, "stats": stats, "type": "video", "type_tag": "视频", "type_icon": "fa-circle-play", - "author_id": str(video_info.owner.mid), # 强制通过 extra 传递 UID - "content_id": video_info.bvid, # 传递 BV 号 + "author_id": str(video_info.owner.mid), + "content_id": video_info.bvid, } # 创建结果 @@ -209,23 +211,21 @@ async def download_video(): author=author, contents=[video_content], ) - - # 强制注入 extra + + # 注入 extra res.extra = extra_data - logger.info(extra_data) + logger.debug(f"Video extra data: {extra_data}") return res - # --- 修改 parse_dynamic --- async def parse_dynamic(self, dynamic_id: int): + """解析动态信息""" from bilibili_api.dynamic import Dynamic - from .dynamic import DynamicData dynamic = Dynamic(dynamic_id, await self.credential) dynamic_data = convert(await dynamic.get_info(), DynamicData) dynamic_info = dynamic_data.item - - # 使用位置参数 + author = self.create_author(dynamic_info.name, dynamic_info.avatar) # 提取动态统计数据 @@ -241,18 +241,19 @@ async def parse_dynamic(self, dynamic_id: int): except Exception: pass + # 下载图片 contents: list[MediaContent] = [] for image_url in dynamic_info.image_urls: img_task = DOWNLOADER.download_img(image_url, ext_headers=self.headers) contents.append(ImageContent(img_task)) - logger.debug(f"dynamic_info: {dynamic_info}") + extra_data = { "stats": stats, "type": "dynamic", "type_tag": "动态", "type_icon": "fa-quote-left", "author_id": str(dynamic_info.modules.module_author.mid), - "content_id": str(dynamic_id), # 传递动态 ID + "content_id": str(dynamic_id), } res = self.result( @@ -266,58 +267,21 @@ async def parse_dynamic(self, dynamic_id: int): return res async def parse_opus(self, opus_id: int): + """解析图文动态信息 (Opus)""" opus = Opus(opus_id, await self.credential) return await self._parse_opus_obj(opus) - async def parse_read_old(self, read_id: int): - from bilibili_api.article import Article - - article = Article(read_id) - return await self._parse_opus_obj(await article.turn_to_opus()) - - async def _parse_opus_obj(self, bili_opus: Opus): - from .opus import OpusItem, TextNode, ImageNode - - opus_info = await bili_opus.get_info() - if not isinstance(opus_info, dict): - raise ParseException("获取图文动态信息失败") - - opus_data = convert(opus_info, OpusItem) - logger.debug(f"opus_data: {opus_data}") - - # 使用位置参数解包 - author = self.create_author(*opus_data.name_avatar) - - contents: list[MediaContent] = [] - current_text = "" - - for node in opus_data.gen_text_img(): - if isinstance(node, ImageNode): - contents.append(self.create_graphics_content(node.url, current_text.strip(), node.alt)) - current_text = "" - elif isinstance(node, TextNode): - current_text += node.text - - res = self.result( - title=opus_data.title, - author=author, - timestamp=opus_data.timestamp, - contents=contents, - text=current_text.strip(), - ) - return res - - # --- 修改 parse_read (专栏) --- async def parse_read(self, read_id: int): + """解析专栏信息 (Article API)""" from bilibili_api.article import Article - from .article import TextNode, ImageNode, ArticleInfo ar = Article(read_id) + # 获取内容,这里需要注意 bilibili-api 的版本,部分版本是 fetch_content await ar.fetch_content() data = ar.json() article_info = convert(data, ArticleInfo) - + stats = {} try: if article_info.stats: @@ -341,18 +305,17 @@ async def parse_read(self, read_id: int): elif isinstance(child, TextNode): current_text += child.text - # 使用位置参数解包 author = self.create_author(*article_info.author_info) - + extra_data = { "stats": stats, "type": "article", "type_tag": "专栏", "type_icon": "fa-newspaper", "author_id": str(article_info.meta.author.mid), - "content_id": f"CV{read_id}", # 传递 cv 号 + "content_id": f"CV{read_id}", } - logger.debug(f"article_info: {article_info}") + res = self.result( title=article_info.title, timestamp=article_info.timestamp, @@ -363,9 +326,48 @@ async def parse_read(self, read_id: int): res.extra = extra_data return res + async def parse_read_with_opus(self, read_id: int): + """解析专栏信息, 使用 Opus 接口 (保留备用)""" + from bilibili_api.article import Article + + article = Article(read_id) + return await self._parse_opus_obj(await article.turn_to_opus()) + + async def _parse_opus_obj(self, bili_opus: Opus): + """解析图文动态/Opus对象""" + from .opus import OpusItem, TextNode, ImageNode + + opus_info = await bili_opus.get_info() + if not isinstance(opus_info, dict): + raise ParseException("获取图文动态信息失败") + + opus_data = convert(opus_info, OpusItem) + logger.debug(f"opus_data: {opus_data}") + + # 使用位置参数解包 + author = self.create_author(*opus_data.name_avatar) + + contents: list[MediaContent] = [] + current_text = "" + + for node in opus_data.gen_text_img(): + if isinstance(node, ImageNode): + contents.append(self.create_graphics_content(node.url, current_text.strip(), node.alt)) + current_text = "" + elif isinstance(node, TextNode): + current_text += node.text + + return self.result( + title=opus_data.title, + author=author, + timestamp=opus_data.timestamp, + contents=contents, + text=current_text.strip(), + ) + async def parse_live(self, room_id: int): + """解析直播信息""" from bilibili_api.live import LiveRoom - from .live import RoomData room = LiveRoom(room_display_id=room_id, credential=await self.credential) @@ -381,24 +383,23 @@ async def parse_live(self, room_id: int): keyframe_task = DOWNLOADER.download_img(keyframe, ext_headers=self.headers) contents.append(ImageContent(keyframe_task)) - # 位置参数 author = self.create_author(room_data.name, room_data.avatar) url = f"https://www.bilibili.com/blackboard/live/live-activity-player.html?enterTheRoom=0&cid={room_id}" - logger.debug(f"room_data: {room_data}") + extra_data = { "type": "live", "type_tag": f"直播·{room_data.room_info.parent_area_name}", "type_icon": "fa-tower-broadcast", - "content_id": f"ROOM{room_id}", # 传递房间号 + "content_id": f"ROOM{room_id}", "tags": str(room_data.room_info.tags), "live_info": { "level": str(room_data.anchor_info.live_info.level), "level_color": str(room_data.anchor_info.live_info.level_color), - "score": str(room_data.anchor_info.live_info.score), - }, + "score": str(room_data.anchor_info.live_info.score) + } } - + res = self.result( url=url, title=room_data.title, @@ -410,10 +411,11 @@ async def parse_live(self, room_id: int): return res async def parse_favlist(self, fav_id: int): + """解析收藏夹信息""" from bilibili_api.favorite_list import get_video_favorite_list_content - from .favlist import FavData + # 只会取一页,20 个 fav_dict = await get_video_favorite_list_content(fav_id) if fav_dict["medias"] is None: @@ -421,7 +423,6 @@ async def parse_favlist(self, fav_id: int): favdata = convert(fav_dict, FavData) - # 位置参数 author = self.create_author(favdata.info.upper.name, favdata.info.upper.face) return self.result( @@ -431,8 +432,8 @@ async def parse_favlist(self, fav_id: int): contents=[self.create_graphics_content(fav.cover, fav.desc) for fav in favdata.medias], ) - # ... 后面的辅助方法保持不变 (_get_video, extract_download_urls, _save_credential 等) ... async def _get_video(self, *, bvid: str | None = None, avid: int | None = None) -> Video: + """获取 Video 对象 (通用 helper)""" if avid: return Video(aid=avid, credential=await self.credential) elif bvid: @@ -448,6 +449,8 @@ async def extract_download_urls( avid: int | None = None, page_index: int = 0, ) -> tuple[str, str | None]: + """解析视频下载链接 (保持最新逻辑)""" + from bilibili_api.video import ( AudioStreamDownloadURL, VideoStreamDownloadURL, @@ -457,6 +460,7 @@ async def extract_download_urls( if video is None: video = await self._get_video(bvid=bvid, avid=avid) + # 获取下载数据 download_url_data = await video.get_download_url(page_index=page_index) detecter = VideoDownloadURLDataDetecter(download_url_data) streams = detecter.detect_best_streams( @@ -477,23 +481,31 @@ async def extract_download_urls( return video_stream.url, audio_stream.url def _save_credential(self): + """存储哔哩哔哩登录凭证""" if self._credential is None: return + self._cookies_file.write_text(json.dumps(self._credential.get_cookies())) def _load_credential(self): + """从文件加载哔哩哔哩登录凭证""" if not self._cookies_file.exists(): return + self._credential = Credential.from_cookies(json.loads(self._cookies_file.read_text())) async def login_with_qrcode(self) -> bytes: + """通过二维码登录获取哔哩哔哩登录凭证""" self._qr_login = QrCodeLogin() await self._qr_login.generate_qrcode() + qr_pic = self._qr_login.get_qrcode_picture() return qr_pic.content async def check_qr_state(self) -> AsyncGenerator[str]: + """检查二维码登录状态""" scan_tip_pending = True + for _ in range(30): state = await self._qr_login.check_state() match state: @@ -514,6 +526,7 @@ async def check_qr_state(self) -> AsyncGenerator[str]: yield "二维码登录超时, 请重新生成" async def _init_credential(self): + """初始化哔哩哔哩登录凭证""" if pconfig.bili_ck is None: self._load_credential() return @@ -529,6 +542,8 @@ async def _init_credential(self): @property async def credential(self) -> Credential | None: + """哔哩哔哩登录凭证""" + if self._credential is None: await self._init_credential() return self._credential From e92270e3027f68bfdbe165c866705a413e33cc4f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 14:40:46 +0000 Subject: [PATCH 14/42] chore: auto fix by pre-commit hooks --- .../parsers/bilibili/__init__.py | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index 844927e6..6aa9110d 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -1,7 +1,7 @@ import json import asyncio from re import Match -from typing import ClassVar, Any +from typing import Any, ClassVar from collections.abc import AsyncGenerator from msgspec import convert @@ -33,10 +33,7 @@ class BilibiliParser(BaseParser): # 平台信息 - platform: ClassVar[Platform] = Platform( - name=PlatformEnum.BILIBILI, - display_name="哔哩哔哩" - ) + platform: ClassVar[Platform] = Platform(name=PlatformEnum.BILIBILI, display_name="哔哩哔哩") def __init__(self): self.headers = HEADERS.copy() @@ -123,7 +120,7 @@ async def parse_video( video = await self._get_video(bvid=bvid, avid=avid) info_data = await video.get_info() video_info = convert(info_data, VideoInfo) - + # 1. 提取统计数据 stats = {} try: @@ -143,7 +140,7 @@ async def parse_video( # 获取简介 text = f"简介: {video_info.desc}" if video_info.desc else None - + # 使用位置参数调用 create_author author = self.create_author(video_info.owner.name, video_info.owner.face) @@ -190,7 +187,7 @@ async def download_video(): page_info.cover, page_info.duration, ) - + # 构造 extra_data extra_data = { "info": ai_summary, @@ -211,7 +208,7 @@ async def download_video(): author=author, contents=[video_content], ) - + # 注入 extra res.extra = extra_data logger.debug(f"Video extra data: {extra_data}") @@ -220,12 +217,13 @@ async def download_video(): async def parse_dynamic(self, dynamic_id: int): """解析动态信息""" from bilibili_api.dynamic import Dynamic + from .dynamic import DynamicData dynamic = Dynamic(dynamic_id, await self.credential) dynamic_data = convert(await dynamic.get_info(), DynamicData) dynamic_info = dynamic_data.item - + author = self.create_author(dynamic_info.name, dynamic_info.avatar) # 提取动态统计数据 @@ -246,7 +244,7 @@ async def parse_dynamic(self, dynamic_id: int): for image_url in dynamic_info.image_urls: img_task = DOWNLOADER.download_img(image_url, ext_headers=self.headers) contents.append(ImageContent(img_task)) - + extra_data = { "stats": stats, "type": "dynamic", @@ -274,6 +272,7 @@ async def parse_opus(self, opus_id: int): async def parse_read(self, read_id: int): """解析专栏信息 (Article API)""" from bilibili_api.article import Article + from .article import TextNode, ImageNode, ArticleInfo ar = Article(read_id) @@ -281,7 +280,7 @@ async def parse_read(self, read_id: int): await ar.fetch_content() data = ar.json() article_info = convert(data, ArticleInfo) - + stats = {} try: if article_info.stats: @@ -306,7 +305,7 @@ async def parse_read(self, read_id: int): current_text += child.text author = self.create_author(*article_info.author_info) - + extra_data = { "stats": stats, "type": "article", @@ -315,7 +314,7 @@ async def parse_read(self, read_id: int): "author_id": str(article_info.meta.author.mid), "content_id": f"CV{read_id}", } - + res = self.result( title=article_info.title, timestamp=article_info.timestamp, @@ -340,10 +339,10 @@ async def _parse_opus_obj(self, bili_opus: Opus): opus_info = await bili_opus.get_info() if not isinstance(opus_info, dict): raise ParseException("获取图文动态信息失败") - + opus_data = convert(opus_info, OpusItem) logger.debug(f"opus_data: {opus_data}") - + # 使用位置参数解包 author = self.create_author(*opus_data.name_avatar) @@ -368,6 +367,7 @@ async def _parse_opus_obj(self, bili_opus: Opus): async def parse_live(self, room_id: int): """解析直播信息""" from bilibili_api.live import LiveRoom + from .live import RoomData room = LiveRoom(room_display_id=room_id, credential=await self.credential) @@ -386,7 +386,7 @@ async def parse_live(self, room_id: int): author = self.create_author(room_data.name, room_data.avatar) url = f"https://www.bilibili.com/blackboard/live/live-activity-player.html?enterTheRoom=0&cid={room_id}" - + extra_data = { "type": "live", "type_tag": f"直播·{room_data.room_info.parent_area_name}", @@ -396,10 +396,10 @@ async def parse_live(self, room_id: int): "live_info": { "level": str(room_data.anchor_info.live_info.level), "level_color": str(room_data.anchor_info.live_info.level_color), - "score": str(room_data.anchor_info.live_info.score) - } + "score": str(room_data.anchor_info.live_info.score), + }, } - + res = self.result( url=url, title=room_data.title, @@ -413,6 +413,7 @@ async def parse_live(self, room_id: int): async def parse_favlist(self, fav_id: int): """解析收藏夹信息""" from bilibili_api.favorite_list import get_video_favorite_list_content + from .favlist import FavData # 只会取一页,20 个 From eeec04bdbdce80b411de269cd882d7c7ca59282d Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:46:11 +0800 Subject: [PATCH 15/42] Update __init__.py --- src/nonebot_plugin_parser/parsers/bilibili/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index 6aa9110d..a29ff5d3 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -1,7 +1,7 @@ import json import asyncio from re import Match -from typing import Any, ClassVar +from typing import ClassVar from collections.abc import AsyncGenerator from msgspec import convert From 61a880cf9316478d667e12068663d8ec3c3f4680 Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Sat, 3 Jan 2026 01:30:45 +0800 Subject: [PATCH 16/42] =?UTF-8?q?=E4=BC=98=E5=85=88=E4=BB=8E=20modules.mod?= =?UTF-8?q?ule=5Fdynamic.desc.text=20=E8=8E=B7=E5=8F=96=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 确保转发内容 --- .../parsers/bilibili/dynamic.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py b/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py index 4a5c84df..8eb70cf9 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from msgspec import Struct, convert @@ -11,11 +11,6 @@ class AuthorInfo(Struct): mid: int pub_time: str pub_ts: int - # jump_url: str - # following: bool = False - # official_verify: dict[str, Any] | None = None - # vip: dict[str, Any] | None = None - # pendant: dict[str, Any] | None = None class VideoArchive(Struct): @@ -26,28 +21,18 @@ class VideoArchive(Struct): title: str desc: str cover: str - # duration_text: str - # jump_url: str - # stat: dict[str, str] - # badge: dict[str, Any] | None = None class OpusImage(Struct): """图文动态图片信息""" url: str - # width: int - # height: int - # size: float - # aigc: dict[str, Any] | None = None - # live_url: str | None = None class OpusSummary(Struct): """图文动态摘要""" text: str - # rich_text_nodes: list[dict[str, Any]] class OpusContent(Struct): @@ -57,11 +42,10 @@ class OpusContent(Struct): pics: list[OpusImage] summary: OpusSummary title: str | None = None - # fold_action: list[str] | None = None class DynamicMajor(Struct): - """动态主要内容""" + """动态主要内容 (Major)""" type: str archive: VideoArchive | None = None @@ -138,6 +122,8 @@ class DynamicInfo(Struct): visible: bool modules: DynamicModule basic: dict[str, Any] | None = None + # 【关键修改】添加 orig 字段以支持转发内容 (递归结构) + orig: Optional["DynamicInfo"] = None @property def name(self) -> str: @@ -161,15 +147,29 @@ def title(self) -> str | None: if major_info: major = convert(major_info, DynamicMajor) return major.title + # 如果是转发动态且没有 major title,可以返回默认值 + if self.type == "DYNAMIC_TYPE_FORWARD": + return "转发动态" return None @property def text(self) -> str | None: """获取文本内容""" + # 【关键修改】优先从 modules.module_dynamic.desc.text 获取 + # 这是用户发布的文字(包括转发时的评论) + if self.modules.module_dynamic: + desc = self.modules.module_dynamic.get("desc") + if desc and isinstance(desc, dict): + text_content = desc.get("text") + if text_content: + return text_content + + # 如果没有直接的 desc 文本,尝试从 major 中获取 (例如纯视频投稿的简介) major_info = self.modules.major_info if major_info: major = convert(major_info, DynamicMajor) return major.text + return None @property From 55baba5b93b2d6206d58419187aadc412c330bc3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:32:22 +0000 Subject: [PATCH 17/42] chore: auto fix by pre-commit hooks --- src/nonebot_plugin_parser/parsers/bilibili/dynamic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py b/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py index 8eb70cf9..a222cbba 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/dynamic.py @@ -169,7 +169,7 @@ def text(self) -> str | None: if major_info: major = convert(major_info, DynamicMajor) return major.text - + return None @property From 81d10aec4a9cf9d0a87a0f76b2eb5e59a9a3db08 Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Sat, 3 Jan 2026 01:32:27 +0800 Subject: [PATCH 18/42] Enhance dynamic parsing with logging and origin handling Added logging for dynamic data and dynamic info, updated comments for clarity, and enhanced origin handling for forwarded dynamics. --- .../parsers/bilibili/__init__.py | 55 +++++++++++++++++-- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index a29ff5d3..b8f9866a 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -217,16 +217,17 @@ async def download_video(): async def parse_dynamic(self, dynamic_id: int): """解析动态信息""" from bilibili_api.dynamic import Dynamic - from .dynamic import DynamicData dynamic = Dynamic(dynamic_id, await self.credential) dynamic_data = convert(await dynamic.get_info(), DynamicData) + logger.debug(f"dynamic_data: {dynamic_data}") dynamic_info = dynamic_data.item - + logger.debug(f"dynamic_info: {dynamic_info}") + author = self.create_author(dynamic_info.name, dynamic_info.avatar) - # 提取动态统计数据 + # 提取当前动态的统计数据 stats = {} try: if dynamic_info.modules.module_stat: @@ -239,12 +240,13 @@ async def parse_dynamic(self, dynamic_id: int): except Exception: pass - # 下载图片 + # 下载当前动态的图片 contents: list[MediaContent] = [] for image_url in dynamic_info.image_urls: img_task = DOWNLOADER.download_img(image_url, ext_headers=self.headers) contents.append(ImageContent(img_task)) - + + # --- 基础 extra 数据 --- extra_data = { "stats": stats, "type": "dynamic", @@ -254,13 +256,54 @@ async def parse_dynamic(self, dynamic_id: int): "content_id": str(dynamic_id), } + # --- 新增:处理转发内容 (叠加到 extra) --- + if dynamic_info.type == "DYNAMIC_TYPE_FORWARD" and dynamic_info.orig: + orig_item = dynamic_info.orig + + if orig_item.visible: + # 尝试获取源内容的类型标签 + orig_type_tag = "动态" + major_info = orig_item.modules.major_info + if major_info: + major_type = major_info.get("type") + if major_type == "MAJOR_TYPE_ARCHIVE": + orig_type_tag = "视频" + elif major_type == "MAJOR_TYPE_OPUS": + orig_type_tag = "专栏" + + # 获取封面图:优先取视频/专栏封面,没有则取第一张配图 + orig_cover = orig_item.cover_url + if not orig_cover and orig_item.image_urls: + orig_cover = orig_item.image_urls[0] + + # 构造 origin 字典 + extra_data["origin"] = { + "exists": True, # 标记源动态存在 + "author": orig_item.name, # 源作者名 + "title": orig_item.title, # 源标题 (如视频标题) + "text": orig_item.text, # 源文本 (如视频简介或动态正文) + "cover": orig_cover, # 源封面/配图 URL (字符串) + "type_tag": orig_type_tag, # 类型标签 + "mid": str(orig_item.modules.module_author.mid) # 源作者ID + } + else: + # 源动态已失效 + extra_data["origin"] = { + "exists": False, + "text": "源动态已被删除或不可见", + "author": "未知", + "title": "资源失效" + } + res = self.result( title=dynamic_info.title or "B站动态", - text=dynamic_info.text, + text=dynamic_info.text, timestamp=dynamic_info.timestamp, author=author, contents=contents, ) + + # 将构造好的 extra_data (包含 origin) 注入结果 res.extra = extra_data return res From ea8d00f1a12f15a13a4e4add9120e3ec91cab580 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:34:06 +0000 Subject: [PATCH 19/42] chore: auto fix by pre-commit hooks --- .../parsers/bilibili/__init__.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py index b8f9866a..6928d25c 100644 --- a/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +++ b/src/nonebot_plugin_parser/parsers/bilibili/__init__.py @@ -217,6 +217,7 @@ async def download_video(): async def parse_dynamic(self, dynamic_id: int): """解析动态信息""" from bilibili_api.dynamic import Dynamic + from .dynamic import DynamicData dynamic = Dynamic(dynamic_id, await self.credential) @@ -224,7 +225,7 @@ async def parse_dynamic(self, dynamic_id: int): logger.debug(f"dynamic_data: {dynamic_data}") dynamic_info = dynamic_data.item logger.debug(f"dynamic_info: {dynamic_info}") - + author = self.create_author(dynamic_info.name, dynamic_info.avatar) # 提取当前动态的统计数据 @@ -245,7 +246,7 @@ async def parse_dynamic(self, dynamic_id: int): for image_url in dynamic_info.image_urls: img_task = DOWNLOADER.download_img(image_url, ext_headers=self.headers) contents.append(ImageContent(img_task)) - + # --- 基础 extra 数据 --- extra_data = { "stats": stats, @@ -259,7 +260,7 @@ async def parse_dynamic(self, dynamic_id: int): # --- 新增:处理转发内容 (叠加到 extra) --- if dynamic_info.type == "DYNAMIC_TYPE_FORWARD" and dynamic_info.orig: orig_item = dynamic_info.orig - + if orig_item.visible: # 尝试获取源内容的类型标签 orig_type_tag = "动态" @@ -278,13 +279,13 @@ async def parse_dynamic(self, dynamic_id: int): # 构造 origin 字典 extra_data["origin"] = { - "exists": True, # 标记源动态存在 - "author": orig_item.name, # 源作者名 - "title": orig_item.title, # 源标题 (如视频标题) - "text": orig_item.text, # 源文本 (如视频简介或动态正文) - "cover": orig_cover, # 源封面/配图 URL (字符串) - "type_tag": orig_type_tag, # 类型标签 - "mid": str(orig_item.modules.module_author.mid) # 源作者ID + "exists": True, # 标记源动态存在 + "author": orig_item.name, # 源作者名 + "title": orig_item.title, # 源标题 (如视频标题) + "text": orig_item.text, # 源文本 (如视频简介或动态正文) + "cover": orig_cover, # 源封面/配图 URL (字符串) + "type_tag": orig_type_tag, # 类型标签 + "mid": str(orig_item.modules.module_author.mid), # 源作者ID } else: # 源动态已失效 @@ -292,17 +293,17 @@ async def parse_dynamic(self, dynamic_id: int): "exists": False, "text": "源动态已被删除或不可见", "author": "未知", - "title": "资源失效" + "title": "资源失效", } res = self.result( title=dynamic_info.title or "B站动态", - text=dynamic_info.text, + text=dynamic_info.text, timestamp=dynamic_info.timestamp, author=author, contents=contents, ) - + # 将构造好的 extra_data (包含 origin) 注入结果 res.extra = extra_data return res From 78e58459d754e0ff90ff19f297eb9f37845d5738 Mon Sep 17 00:00:00 2001 From: LoCCai <105721394+LoCCai@users.noreply.github.com> Date: Sat, 3 Jan 2026 01:34:47 +0800 Subject: [PATCH 20/42] =?UTF-8?q?=E8=BD=AC=E5=8F=91=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../renders/templates/bilibili.html.jinja | 305 +++++++++--------- 1 file changed, 158 insertions(+), 147 deletions(-) diff --git a/src/nonebot_plugin_parser/renders/templates/bilibili.html.jinja b/src/nonebot_plugin_parser/renders/templates/bilibili.html.jinja index cbacf4ce..f3528aa4 100644 --- a/src/nonebot_plugin_parser/renders/templates/bilibili.html.jinja +++ b/src/nonebot_plugin_parser/renders/templates/bilibili.html.jinja @@ -31,7 +31,6 @@ justify-content: center; align-items: flex-start; min-height: 100vh; - /* 透明背景,方便截图工具处理 */ background: transparent; padding: 20px; } @@ -62,7 +61,6 @@ .cover-section { position: relative; width: 100%; - /* 限制最大高度,防止长图占满屏幕 */ max-height: 400px; background-color: #f0f2f5; overflow: hidden; @@ -112,10 +110,6 @@ border-left: 3px solid var(--secondary-color); } - .platform-logo-img { - - } - /* 播放按钮装饰 */ .play-btn-overlay { position: absolute; @@ -170,16 +164,29 @@ font-size: 13px; display: flex; align-items: center; - gap: 10px; + gap: 8px; + flex-wrap: wrap; } - .mid-badge { + .info-badge { background-color: #f1f8ff; padding: 2px 8px; border-radius: 6px; - color: var(--secondary-color); font-weight: 600; - font-family: monospace; + font-family: Consolas, Monaco, monospace; + font-size: 12px; + } + + .info-badge.uid { color: var(--secondary-color); background-color: #eaf4fc; } + .info-badge.content-id { color: #e67e22; background-color: #fdf2e9; } + + .time-text { + display: flex; align-items: center; gap: 4px; + } + .time-text::before { + content: "\f017"; + font-family: "Font Awesome 6 Free"; + font-weight: 400; } .qrcode-placeholder { @@ -188,7 +195,7 @@ } .qrcode-icon { font-size: 32px; color: #eee; } - /* 统计数据区域 - 网格布局 */ + /* 统计数据区域 */ .stats-container { display: grid; grid-template-columns: repeat(3, 1fr); @@ -206,12 +213,12 @@ border-left: 4px solid transparent; } - .stat-item:nth-child(1) { border-left-color: #e74c3c; } /* Play */ - .stat-item:nth-child(2) { border-left-color: #3498db; } /* Like */ - .stat-item:nth-child(3) { border-left-color: #2ecc71; } /* Reply */ - .stat-item:nth-child(4) { border-left-color: #f39c12; } /* Fav */ - .stat-item:nth-child(5) { border-left-color: #9b59b6; } /* Share */ - .stat-item:nth-child(6) { border-left-color: #1abc9c; } /* Coin/Danmaku */ + .stat-item:nth-child(1) { border-left-color: #e74c3c; } + .stat-item:nth-child(2) { border-left-color: #3498db; } + .stat-item:nth-child(3) { border-left-color: #2ecc71; } + .stat-item:nth-child(4) { border-left-color: #f39c12; } + .stat-item:nth-child(5) { border-left-color: #9b59b6; } + .stat-item:nth-child(6) { border-left-color: #1abc9c; } .stat-icon { width: 36px; height: 36px; @@ -242,38 +249,7 @@ padding-left: 12px; border-left: 4px solid var(--secondary-color); } - /* 优化作者信息栏的 Badge 样式 */ - .author-meta { - color: var(--gray-color); - font-size: 13px; - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; /* 防止太长换行难看 */ - } - - .info-badge { - background-color: #f1f8ff; - padding: 2px 8px; - border-radius: 6px; - font-weight: 600; - font-family: Consolas, Monaco, monospace; /* 使用等宽字体显示 ID */ - font-size: 12px; - } - /* 区分一下颜色 */ - .info-badge.uid { color: var(--secondary-color); background-color: #eaf4fc; } - .info-badge.content-id { color: #e67e22; background-color: #fdf2e9; } - - .time-text { - display: flex; align-items: center; gap: 4px; - } - .time-text::before { - content: "\f017"; /* fa-clock */ - font-family: "Font Awesome 6 Free"; - font-weight: 400; - } - /* AI 总结样式 */ .ai-summary { background-color: #f8faff; padding: 16px 20px; @@ -301,6 +277,88 @@ white-space: pre-wrap; } + /* ----------------------- + 【新增】转发区域样式 + ----------------------- */ + .repost-box { + background-color: #f7f9fa; + border-radius: 8px; + padding: 16px; + margin: 20px 0; + border: 1px solid #e1e4e8; + position: relative; + } + + .repost-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; + padding-bottom: 10px; + border-bottom: 1px dashed #e0e0e0; + } + + .repost-tag { + background-color: var(--secondary-color); + color: white; + font-size: 11px; + padding: 2px 8px; + border-radius: 4px; + font-weight: bold; + } + + .repost-author { + color: var(--secondary-color); + font-weight: 700; + font-size: 14px; + } + + .repost-title { + font-size: 16px; + font-weight: 700; + color: var(--dark-color); + margin-bottom: 8px; + line-height: 1.4; + } + + .repost-content { + font-size: 14px; + color: #555; + line-height: 1.6; + margin-bottom: 12px; + white-space: pre-wrap; + } + + .repost-media { + width: 100%; + border-radius: 6px; + overflow: hidden; + background-color: #eee; + margin-top: 10px; + } + + .repost-media img { + width: 100%; + height: auto; + max-height: 300px; + object-fit: cover; + display: block; + } + + .repost-deleted { + background-color: #f8eeee; + color: #999; + padding: 15px; + border-radius: 8px; + text-align: center; + font-size: 14px; + border: 1px dashed #ddd; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + } + /* 图片展示区域 */ .images-container { margin: 20px 0; } .images-header { @@ -328,20 +386,6 @@ font-size: 28px; font-weight: bold; } - /* 转发卡片样式 */ - .repost-container { - margin-top: 20px; - border: 1px solid #eee; - background-color: #fafbfc; - border-radius: 8px; - padding: 15px; - } - .repost-container .card { - width: 100%; box-shadow: none; border: none; margin: 0; background: transparent; - } - .repost-container .content-section { padding: 0; } - .repost-container .cover-section { border-radius: 8px; margin-bottom: 15px; } - /* 底部信息栏 */ .footer-section { background-color: var(--dark-color); @@ -361,11 +405,9 @@ {# === 宏定义:渲染卡片 === #} {% macro render_card(result, is_repost=False) %} - {# 提取 Extra 数据 (由 Parser 修改后注入) #} - {# 获取变量 #} + {# 变量定义 #} {% set extra = result.extra if result.extra else {} %} {% set platform = result.platform if result.platform else {} %} - {# 获取 Content ID #} {% set content_id = extra.content_id %} {% set stats = extra.stats if extra.stats else {} %} {% set type_tag = extra.type_tag if extra.type_tag else "B站" %} @@ -373,13 +415,11 @@ {% set type = extra.type if extra.type else "video" %}
- {# 仅主卡片显示顶部彩条 #} {% if not is_repost %}
{% endif %} {# --- 封面区域 --- #} - {# 优先使用提取到的封面,如果是 repost 且没有封面,尝试使用第一张图片 #} {% set cover_url = result.cover_path %} {% if not cover_url and result.img_contents and result.img_contents|length > 0 %} {% set cover_url = result.img_contents[0].path %} @@ -390,8 +430,6 @@ Cover
- {# 【修改点 1】只有 B 站才显示左侧的 类型标签 (视频/专栏等) #} - {# 因为其他平台可能没有 type_tag 这个字段或者不需要显示 #} {% if platform.name == 'bilibili' and extra.type_tag %}
@@ -399,7 +437,6 @@
{% endif %} - {# 右上角:平台标签 (一直显示) #} {% if platform %}
{% if result.platform.logo_path %} @@ -410,7 +447,6 @@
{% endif %} - {# 中间:播放按钮装饰 (仅视频/音频) #} {% if type in ['video', 'live'] %}
@@ -425,101 +461,48 @@
-

{{ result.author.name }}

- {# 显示 UID #} {% if extra.author_id %} UID: {{ extra.author_id }} {% endif %} - - {# 【修改点 2】显示内容 ID (BV/CV/RoomID) #} {% if content_id %} {{ content_id }} {% endif %} - - {# 时间 #} {% if result.formartted_datetime %} {{ result.formartted_datetime }} {% endif %}
- - {# 装饰性二维码图标 #}
{% endif %} - {# --- 统计数据网格 (仅主卡片显示) --- #} + {# --- 统计数据 --- #} {% if stats and not is_repost %}
- +
{{ stats.play or '-' }}
播放
+
{{ stats.like or '-' }}
点赞
+
{{ stats.reply or '-' }}
评论
+
{{ stats.favorite or '-' }}
收藏
+
{{ stats.share or '-' }}
分享
-
-
-
{{ stats.play or '-' }}
-
播放
-
-
- -
- -
-
{{ stats.like or '-' }}
-
点赞
-
-
- -
-
-
-
{{ stats.reply or '-' }}
-
评论
-
-
- -
-
-
-
{{ stats.favorite or '-' }}
-
收藏
-
-
- -
- -
-
{{ stats.share or '-' }}
-
分享
-
-
- -
-
- {% if stats.coin %} - - {% else %} - - {% endif %} -
-
-
{{ stats.coin or stats.danmaku or '-' }}
-
{{ '硬币' if stats.coin else '弹幕' }}
-
+
{% if stats.coin %}{% else %}{% endif %}
+
{{ stats.coin or stats.danmaku or '-' }}
{{ '硬币' if stats.coin else '弹幕' }}
{% endif %} - {# --- 主要内容 --- #} + {# --- 主要标题 --- #} {% if result.title %}
{{ result.title }}
{% endif %} - {# AI 总结 #} + {# --- AI 总结 --- #} {% if extra.info %}

AI 内容总结

@@ -527,24 +510,54 @@
{% endif %} - {# 正文描述 #} + {# --- 正文描述 --- #} {% if result.text %}
{{ result.text }}
{% endif %} - {# --- 图片网格 (除去封面) --- #} - {# 如果第一张图被用作封面了,这里可能需要排除它,但通常动态图多张都需要展示 #} - {# 这里逻辑:如果有图片内容,且不是视频(视频封面已在上面展示) #} + {# --- 【新增】Extra Origin 转发区域 --- #} + {% if result.extra.origin %} +
+ {% if result.extra.origin.exists %} +
+ {{ result.extra.origin.type_tag }} + @{{ result.extra.origin.author }} + {% if result.extra.origin.mid %} + (UID: {{ result.extra.origin.mid }}) + {% endif %} +
+ + {% if result.extra.origin.title %} +
{{ result.extra.origin.title }}
+ {% endif %} + + {% if result.extra.origin.text %} +
{{ result.extra.origin.text }}
+ {% endif %} + + {% if result.extra.origin.cover %} +
+ 转发图片 +
+ {% endif %} + {% else %} + +
+ + {{ result.extra.origin.text or '源动态已被删除或不可见' }} +
+ {% endif %} +
+ {% endif %} + + {# --- 图片网格 --- #} {% if result.img_contents %} {% set imgs = result.img_contents %} - {# 如果是视频/专栏,可能封面就是第一张图,这里根据实际情况可以切片 imgs[1:],目前全显示 #} - {% set count = imgs|length %} {% if count > 0 %}
内容预览
- {# 计算网格类名 #} {% set grid_class = 'single' %} {% if count == 2 %}{% set grid_class = 'double' %} {% elif count == 4 %}{% set grid_class = 'quad' %} @@ -552,7 +565,6 @@ {% endif %}
- {# 限制最多显示9张 #} {% for img in imgs[:9] %}
@@ -566,18 +578,18 @@ {% endif %} {% endif %} - {# --- 嵌套转发内容 --- #} + {# --- 旧版 Repost (保留兼容性) --- #} {% if result.repost %} -
+
- 转发内容 + 转发内容 (Legacy)
{{ render_card(result.repost, is_repost=True) }}
{% endif %}
- {# --- 底部信息栏 --- #} + {# --- 底部 --- #} {% if not is_repost %}