refactor(managers): 重构单例管理器实现并优化代码结构

feat(ws_pool): 新增 WebSocket 连接池实现

perf(json): 使用 orjson 替代标准 json 库提升性能

style: 清理未使用的导入和冗余代码

docs: 更新架构文档和开发规范

test: 添加 WebSocket 连接池测试用例

fix(plugins): 修复自动审批插件 API 调用参数格式
This commit is contained in:
2026-01-22 16:23:03 +08:00
parent d7d732ff4d
commit caf5b06097
42 changed files with 1285 additions and 261 deletions

View File

@@ -53,16 +53,16 @@ async def admin_management(event: MessageEvent, args: list[str]):
# 根据子命令分发
if subcommand == "add_admin":
permission_manager.set_user_permission(target_user_id, Permission.ADMIN)
await permission_manager.set_user_permission(target_user_id, Permission.ADMIN)
await event.reply(f"已成功添加管理员:{target_user_id}")
elif subcommand == "remove_admin":
permission_manager.set_user_permission(target_user_id, Permission.USER)
await permission_manager.set_user_permission(target_user_id, Permission.USER)
await event.reply(f"已成功移除管理员:{target_user_id}")
elif subcommand == "add_op":
permission_manager.set_user_permission(target_user_id, Permission.OP)
await permission_manager.set_user_permission(target_user_id, Permission.OP)
await event.reply(f"已成功添加操作员:{target_user_id}")
elif subcommand == "remove_op":
permission_manager.set_user_permission(target_user_id, Permission.USER)
await permission_manager.set_user_permission(target_user_id, Permission.USER)
await event.reply(f"已成功移除操作员:{target_user_id}")
else:
await event.reply(f"未知的子命令 '{subcommand}'\n\n{__plugin_meta__['usage']}")

View File

@@ -25,8 +25,10 @@ async def handle_friend_request(bot: Bot, event: FriendRequestEvent):
# 自动同意好友请求
await bot.call_api(
"set_friend_add_request",
flag=event.flag,
approve=True
params={
"flag": event.flag,
"approve": True
}
)
print(f"[自动同意] 已同意用户 {event.user_id} 的好友请求")
except Exception as e:
@@ -44,9 +46,11 @@ async def handle_group_request(bot: Bot, event: GroupRequestEvent):
# 自动同意群聊邀请
await bot.call_api(
"set_group_add_request",
flag=event.flag,
sub_type=event.sub_type,
approve=True
params={
"flag": event.flag,
"sub_type": event.sub_type,
"approve": True
}
)
print(f"[自动同意] 已同意加入群聊 {event.group_id} (邀请人: {event.user_id})")
except Exception as e:

View File

@@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
import re
import json
import orjson
import abc
import aiohttp
from typing import Optional, Dict, Any, List, Union
from cachetools import TTLCache
from core.utils.logger import logger
from models import MessageEvent, MessageSegment
from models import MessageEvent
class BaseParser(metaclass=abc.ABCMeta):
@@ -38,6 +37,7 @@ class BaseParser(metaclass=abc.ABCMeta):
"""
self.name = "Base Parser"
self.url_pattern = re.compile(r"https?://[^\s]+")
self.processed_messages = {} # 用于存储已处理的消息ID防止重复处理
@classmethod
def get_session(cls) -> aiohttp.ClientSession:
@@ -105,12 +105,12 @@ class BaseParser(metaclass=abc.ABCMeta):
if segment.type == "json":
logger.info(f"[{self.name}] 检测到JSON CQ码: {segment.data}")
try:
json_data = json.loads(segment.data.get("data", "{}"))
json_data = orjson.loads(segment.data.get("data", "{}"))
short_url = json_data.get("meta", {}).get("detail_1", {}).get("qqdocurl")
if short_url:
logger.success(f"[{self.name}] 成功从JSON卡片中提取到链接: {short_url}")
return short_url
except (json.JSONDecodeError, KeyError) as e:
except (orjson.JSONDecodeError, KeyError) as e:
logger.error(f"[{self.name}] 解析JSON失败: {e}")
continue
return None

View File

@@ -1,14 +1,14 @@
# -*- coding: utf-8 -*-
import re
import json
import orjson
import aiohttp
from typing import Optional, Dict, Any, List
from typing import Optional, Dict, Any, List, Union
from bs4 import BeautifulSoup
from core.utils.logger import logger
from models import MessageEvent, MessageSegment
from ..base import BaseParser
from ..utils import format_duration, clean_url
from ..utils import format_duration
from cachetools import TTLCache
@@ -42,7 +42,7 @@ class BiliParser(BaseParser):
clean_url = clean_url.split('#/')[0]
session = self.get_session()
async with session.get(clean_url, headers=self.HEADERS, timeout=5) as response:
async with session.get(clean_url, headers=self.HEADERS, timeout=aiohttp.ClientTimeout(total=5)) as response:
response.raise_for_status()
text = await response.text()
soup = BeautifulSoup(text, 'html.parser')
@@ -93,14 +93,14 @@ class BiliParser(BaseParser):
json_str = json_str.strip().rstrip(';')
try:
data = json.loads(json_str)
except json.JSONDecodeError:
data = orjson.loads(json_str)
except ValueError:
# 如果直接解析失败尝试清理JSON字符串
# 移除可能的注释或无效字符
cleaned_json = re.sub(r',\s*[}]', '}', json_str) # 移除末尾多余的逗号
cleaned_json = re.sub(r'/\*.*?\*/', '', cleaned_json) # 移除注释
cleaned_json = re.sub(r'//.*', '', cleaned_json) # 移除行注释
data = json.loads(cleaned_json)
data = orjson.loads(cleaned_json)
video_data = data.get('videoData', {})
up_data = data.get('upData', {})
@@ -134,7 +134,7 @@ class BiliParser(BaseParser):
"followers": up_data.get('fans', 0),
}
except (aiohttp.ClientError, KeyError, AttributeError, json.JSONDecodeError) as e:
except (aiohttp.ClientError, KeyError, AttributeError, ValueError) as e:
logger.error(f"[{self.name}] 解析视频信息失败: {e}")
logger.debug(f"失败的URL: {url}")
except Exception as e:
@@ -155,7 +155,7 @@ class BiliParser(BaseParser):
"""
try:
session = self.get_session()
async with session.head(short_url, headers=self.HEADERS, allow_redirects=False, timeout=5) as response:
async with session.head(short_url, headers=self.HEADERS, allow_redirects=False, timeout=aiohttp.ClientTimeout(total=5)) as response:
if response.status == 302:
return response.headers.get('Location')
except Exception as e:
@@ -175,13 +175,13 @@ class BiliParser(BaseParser):
api_url = f"https://api.mir6.com/api/bzjiexi?url={video_url}&type=json"
try:
async with aiohttp.ClientSession() as session:
async with session.get(api_url, headers=self.HEADERS, timeout=10) as response:
async with session.get(api_url, headers=self.HEADERS, timeout=aiohttp.ClientTimeout(total=10)) as response:
response.raise_for_status()
# 使用 content_type=None 来忽略 Content-Type 检查
data = await response.json(content_type=None)
if data.get("code") == 200 and data.get("data"):
return data["data"][0].get("video_url")
except (aiohttp.ClientError, json.JSONDecodeError, KeyError, IndexError) as e:
except (aiohttp.ClientError, ValueError, KeyError, IndexError) as e:
logger.error(f"[{self.name}] 调用第三方API解析视频失败: {e}")
return None
@@ -197,6 +197,7 @@ class BiliParser(BaseParser):
List[Any]: 消息段列表
"""
# 检查视频时长
video_message: Union[str, MessageSegment]
if data['duration'] > 1200: # 20分钟 = 1200秒
video_message = "视频时长超过20分钟不进行解析。"
else:

View File

@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
import re
import json
import aiohttp
from typing import Optional, Dict, Any, List
@@ -40,7 +39,7 @@ class DouyinParser(BaseParser):
api_url = f"http://api.xhus.cn/api/douyin?url={url}"
session = self.get_session()
async with session.get(api_url, headers=self.HEADERS, timeout=10) as response:
async with session.get(api_url, headers=self.HEADERS, timeout=aiohttp.ClientTimeout(total=10)) as response:
if response.status != 200:
logger.error(f"[{self.name}] API请求失败状态码: {response.status}")
return None
@@ -75,7 +74,7 @@ class DouyinParser(BaseParser):
"music": data.get("music", {}),
}
except (aiohttp.ClientError, KeyError, AttributeError, json.JSONDecodeError) as e:
except (aiohttp.ClientError, KeyError, AttributeError, ValueError) as e:
logger.error(f"[{self.name}] 解析抖音视频信息失败: {e}")
logger.debug(f"失败的URL: {url}")
except Exception as e:
@@ -110,7 +109,7 @@ class DouyinParser(BaseParser):
'Referer': 'https://www.douyin.com/'
})
async with session.get(short_url, headers=mobile_headers, allow_redirects=True, timeout=10) as response:
async with session.get(short_url, headers=mobile_headers, allow_redirects=True, timeout=aiohttp.ClientTimeout(total=10)) as response:
redirected_url = str(response.url)
# 检查重定向后的URL是否包含视频ID

View File

@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
import re
import json
import aiohttp
from typing import Optional, Dict, Any, List
from cachetools import TTLCache
@@ -60,7 +59,7 @@ class GitHubParser(BaseParser):
"""
try:
session = self.get_session()
async with session.head(short_url, headers=self.HEADERS, allow_redirects=False, timeout=5) as response:
async with session.head(short_url, headers=self.HEADERS, allow_redirects=False, timeout=aiohttp.ClientTimeout(total=5)) as response:
if response.status == 302:
return response.headers.get('Location')
except Exception as e:
@@ -86,7 +85,7 @@ class GitHubParser(BaseParser):
api_url = f"https://api.github.com/repos/{owner}/{repo}"
try:
session = self.get_session()
async with session.get(api_url, timeout=10) as response:
async with session.get(api_url, timeout=aiohttp.ClientTimeout(total=10)) as response:
response.raise_for_status()
repo_data = await response.json()
@@ -97,7 +96,7 @@ class GitHubParser(BaseParser):
except aiohttp.ClientError as e:
logger.error(f"[{self.name}] GitHub API请求失败: {e}")
except json.JSONDecodeError as e:
except ValueError as e:
logger.error(f"[{self.name}] 解析GitHub API响应失败: {e}")
except Exception as e:
logger.error(f"[{self.name}] 获取仓库信息时发生未知错误: {e}")

View File

@@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
import re
import json
from typing import Optional, Dict, Any, Union, List
from typing import Dict, Any, List
from core.utils.logger import logger
from models import MessageEvent, MessageSegment
from models import MessageEvent
def format_duration(seconds: int) -> str: