feat: 添加测试用例并优化代码结构

refactor(permission_manager): 调整初始化顺序和逻辑
fix(admin_manager): 修复初始化逻辑和目录创建问题
feat(ws): 优化Bot实例初始化条件
feat(message): 增强MessageSegment功能并添加测试
feat(events): 支持字符串格式的消息解析
test: 添加核心功能测试用例
refactor(plugin_manager): 改进插件路径处理
style: 清理无用导入和代码
chore: 更新依赖项
This commit is contained in:
2026-01-09 00:20:30 +08:00
parent 5d07a84283
commit 77348113e3
18 changed files with 754 additions and 73 deletions

View File

@@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
import re
import json
import httpx
import requests
from bs4 import BeautifulSoup
from typing import Optional, Dict, Any
from typing import Optional, Dict, Any, Union
from cachetools import TTLCache
from core.utils.logger import logger
from core.managers.command_manager import matcher
from models.events.message import MessageEvent, MessageSegment
from models import MessageEvent, MessageSegment
# 创建一个TTL缓存最大容量100缓存时间60秒
processed_messages: TTLCache[Any, bool] = TTLCache(maxsize=100, ttl=60)
# 创建一个TTL缓存最大容量100缓存时间10秒
processed_messages: TTLCache[int, bool] = TTLCache(maxsize=100, ttl=10)
__plugin_meta__ = {
"name": "bili_parser",
@@ -23,9 +23,6 @@ HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
# 创建可复用的异步HTTP客户端
async_client = httpx.AsyncClient(headers=HEADERS, follow_redirects=False, timeout=10)
def format_count(num: int) -> str:
if not isinstance(num, int):
@@ -43,32 +40,29 @@ def format_duration(seconds: int) -> str:
return f"{minutes:02d}:{seconds:02d}"
async def get_real_url(short_url: str) -> Optional[str]:
def get_real_url(short_url: str) -> Optional[str]:
try:
response = await async_client.head(short_url)
response = requests.head(short_url, headers=HEADERS, allow_redirects=False, timeout=5)
if response.status_code == 302:
return response.headers.get('Location')
except httpx.RequestError as e:
logger.error(f"获取真实URL失败: {e}")
except requests.RequestException as e:
print(f"获取真实URL失败: {e}")
return None
async def parse_video_info(video_url: str) -> Optional[Dict[str, Any]]:
def parse_video_info(video_url: str) -> Optional[Dict[str, Any]]:
try:
response = await async_client.get(video_url, follow_redirects=True)
response = requests.get(video_url, headers=HEADERS, timeout=5)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
script_tag = soup.find('script', text=re.compile('window.__INITIAL_STATE__'))
if not script_tag:
if not script_tag or not script_tag.string:
return None
script_tag_content = script_tag.string
if not script_tag_content:
return None
match = re.search(r'window\.__INITIAL_STATE__\s*=\s*(\{.*?\});', script_tag_content)
match = re.search(r'window\.__INITIAL_STATE__\s*=\s*(\{.*?\});', script_tag.string)
if not match:
return None
json_str = match.group(1)
data = json.loads(json_str)
@@ -104,12 +98,12 @@ async def parse_video_info(video_url: str) -> Optional[Dict[str, Any]]:
"followers": up_data.get('fans', 0),
}
except (httpx.RequestError, KeyError, AttributeError, json.JSONDecodeError) as e:
logger.error(f"解析视频信息失败: {e}")
except (requests.RequestException, KeyError, AttributeError, json.JSONDecodeError) as e:
print(f"解析视频信息失败: {e}")
return None
async def get_direct_video_url(video_url: str) -> Optional[str]:
def get_direct_video_url(video_url: str) -> Optional[str]:
"""
调用第三方API解析B站视频直链
:param video_url: B站视频的完整URL
@@ -117,12 +111,12 @@ async def get_direct_video_url(video_url: str) -> Optional[str]:
"""
api_url = f"https://api.mir6.com/api/bzjiexi?url={video_url}&type=json"
try:
response = await async_client.get(api_url)
response = requests.get(api_url, headers=HEADERS, timeout=10)
response.raise_for_status()
data = response.json()
if data.get("code") == 200 and data.get("data"):
return data["data"][0].get("video_url")
except (httpx.RequestError, json.JSONDecodeError, KeyError, IndexError) as e:
except (requests.RequestException, json.JSONDecodeError, KeyError, IndexError) as e:
logger.error(f"[bili_parser] 调用第三方API解析视频失败: {e}")
return None
@@ -184,7 +178,7 @@ async def process_bili_link(event: MessageEvent, url: str):
:param url: 待处理的B站链接
"""
if "b23.tv" in url:
real_url = await get_real_url(url)
real_url = get_real_url(url)
if not real_url:
logger.error(f"[bili_parser] 无法从 {url} 获取真实URL。")
await event.reply("无法解析B站短链接。")
@@ -192,28 +186,59 @@ async def process_bili_link(event: MessageEvent, url: str):
else:
real_url = url.split('?')[0]
video_info = await parse_video_info(real_url)
video_info = parse_video_info(real_url)
if not video_info:
logger.error(f"[bili_parser] 无法从 {real_url} 解析视频信息。")
await event.reply("无法获取视频信息可能是B站接口变动或视频不存在。")
return
title = video_info.get("title", "未知标题")
owner_name = video_info.get("owner_name", "未知UP主")
cover_url = video_info.get("cover_url")
bvid = video_info.get("bvid", "N/A")
play_count = format_count(video_info.get("play", 0))
like_count = format_count(video_info.get("like", 0))
# 检查视频时长
video_message: Union[str, MessageSegment]
if video_info['duration'] > 300: # 5分钟 = 300秒
video_message = "视频时长超过5分钟不进行解析。"
else:
direct_url = get_direct_video_url(real_url)
if direct_url:
video_message = MessageSegment.video(direct_url)
else:
video_message = "视频解析失败,无法获取直链。"
text_part = (
f"标题: {title}\n"
f"UP主: {owner_name}\n"
f"BV: {bvid} | ▶️ {play_count} | 👍 {like_count}"
text_message = (
f"BiliBili 视频解析\n"
f"--------------------\n"
f" UP主: {video_info['owner_name']}\n"
f" 粉丝: {format_count(video_info['followers'])}\n"
f"--------------------\n"
f" 标题: {video_info['title']}\n"
f" BV号: {video_info['bvid']}\n"
f" 时长: {format_duration(video_info['duration'])}\n"
f"--------------------\n"
f" 数据:\n"
f" 播放: {format_count(video_info['play'])}\n"
f" 点赞: {format_count(video_info['like'])}\n"
f" 投币: {format_count(video_info['coin'])}\n"
f" 收藏: {format_count(video_info['favorite'])}\n"
f" 转发: {format_count(video_info['share'])}\n"
f" B站链接: {url}"
)
reply_message = [MessageSegment.from_text(text_part)]
if cover_url:
reply_message.append(MessageSegment.image(cover_url))
logger.success(f"[bili_parser] 成功解析视频信息并准备回复: {title}")
await event.reply(reply_message)
image_message_segment = [
MessageSegment.text("B站封面"),
MessageSegment.image(video_info['cover_url'])
]
up_info_segment = [
MessageSegment.text("UP主头像"),
MessageSegment.image(video_info['owner_avatar'])
]
nodes = [
event.bot.build_forward_node(user_id=event.self_id, nickname="B站视频解析", message=text_message),
event.bot.build_forward_node(user_id=event.self_id, nickname="B站视频解析", message=image_message_segment),
event.bot.build_forward_node(user_id=event.self_id, nickname="B站视频解析", message=up_info_segment),
event.bot.build_forward_node(user_id=event.self_id, nickname="B站视频解析", message=video_message)
]
logger.success(f"[bili_parser] 成功解析视频信息并准备以聊天记录形式回复: {video_info['title']}")
# 使用更通用的 send_forwarded_messages 方法,自动判断私聊或群聊
await event.bot.send_forwarded_messages(target=event, nodes=nodes)