mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-09 13:50:00 +08:00
性能优化 (#2126)
* 性能优化 * 代码改进 * 优化浏览器代际切换逻辑 * 统一缓存与生命周期 * 添加aiomysql依赖 * 优化插件路径处理逻辑,简化条件判断;在虚拟环境包管理器中添加编码和错误处理参数以增强稳定性 * 🚨 auto fix by pre-commit hooks * 优化Windows下的关闭逻辑 * 代码优化 * bugfix:修复配置重载问题 * bugfix:修复插件加载启动竞态问题 * 收敛事件入口和权限上下文 * 优化 Windows launcher 关闭重启兜底 --------- Co-authored-by: HibiKier <775757368@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
HibiKier
pre-commit-ci[bot]
parent
24c316cd2c
commit
5d92ccd3b0
@@ -55,8 +55,6 @@ class CacheType(StrEnum):
|
||||
"""全局全部群组"""
|
||||
GROUP_PLUGIN_SETTINGS = "GROUP_PLUGIN_SETTINGS"
|
||||
"""插件分群配置"""
|
||||
GROUP_PLUGIN_SETTINGS_VIEW = "GROUP_PLUGIN_SETTINGS_VIEW"
|
||||
"""插件分群配置视图缓存(聚合 dict)"""
|
||||
USERS = "GLOBAL_ALL_USERS"
|
||||
"""全部用户"""
|
||||
BAN = "GLOBAL_ALL_BAN"
|
||||
|
||||
+12
-34
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from collections.abc import AsyncGenerator, Awaitable, Callable, Sequence
|
||||
from contextlib import asynccontextmanager
|
||||
import os
|
||||
@@ -21,6 +20,7 @@ from rich.progress import (
|
||||
import ujson as json
|
||||
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.services.cache.bounded_ttl import BoundedTTLCache
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.decorator.retry import Retry
|
||||
from zhenxun.utils.exception import AllURIsFailedError
|
||||
@@ -168,7 +168,12 @@ class AsyncHttpx:
|
||||
_CONTENT_CACHE_TTL: ClassVar[float] = 3.0
|
||||
_CONTENT_CACHE_MAX_ITEMS: ClassVar[int] = 256
|
||||
_CONTENT_CACHE_MAX_BYTES: ClassVar[int] = 2 * 1024 * 1024
|
||||
_content_cache: ClassVar[OrderedDict[str, tuple[float, bytes]]] = OrderedDict()
|
||||
_content_cache: ClassVar[BoundedTTLCache[str, bytes]] = BoundedTTLCache(
|
||||
"HTTP_IMAGE_CONTENT",
|
||||
ttl_seconds=_CONTENT_CACHE_TTL,
|
||||
max_items=_CONTENT_CACHE_MAX_ITEMS,
|
||||
max_total_bytes=_CONTENT_CACHE_MAX_BYTES,
|
||||
)
|
||||
_content_inflight: ClassVar[dict[str, asyncio.Task[Response]]] = {}
|
||||
_content_cache_lock: ClassVar[asyncio.Lock] = asyncio.Lock()
|
||||
|
||||
@@ -191,29 +196,6 @@ class AsyncHttpx:
|
||||
return True
|
||||
return "qpic.cn" in lower_url or "qlogo.cn" in lower_url
|
||||
|
||||
@classmethod
|
||||
def _get_cached_content_nolock(cls, key: str) -> bytes | None:
|
||||
entry = cls._content_cache.get(key)
|
||||
if not entry:
|
||||
return None
|
||||
expire_at, content = entry
|
||||
if expire_at <= time.monotonic():
|
||||
cls._content_cache.pop(key, None)
|
||||
return None
|
||||
cls._content_cache.move_to_end(key)
|
||||
return content
|
||||
|
||||
@classmethod
|
||||
def _cleanup_content_cache_nolock(cls) -> None:
|
||||
now = time.monotonic()
|
||||
while cls._content_cache:
|
||||
expire_at, _ = next(iter(cls._content_cache.values()))
|
||||
if expire_at > now:
|
||||
break
|
||||
cls._content_cache.popitem(last=False)
|
||||
while len(cls._content_cache) > cls._CONTENT_CACHE_MAX_ITEMS:
|
||||
cls._content_cache.popitem(last=False)
|
||||
|
||||
@classmethod
|
||||
async def _try_cache_content(cls, key: str, response: Response) -> None:
|
||||
content = response.content
|
||||
@@ -223,13 +205,7 @@ class AsyncHttpx:
|
||||
is_image = content_type.startswith("image/") or cls._is_probably_image_url(key)
|
||||
if not is_image:
|
||||
return
|
||||
async with cls._content_cache_lock:
|
||||
cls._content_cache[key] = (
|
||||
time.monotonic() + cls._CONTENT_CACHE_TTL,
|
||||
content,
|
||||
)
|
||||
cls._content_cache.move_to_end(key)
|
||||
cls._cleanup_content_cache_nolock()
|
||||
await cls._content_cache.set(key, content)
|
||||
|
||||
@classmethod
|
||||
def _prepare_temporary_client_config(cls, client_kwargs: dict) -> dict:
|
||||
@@ -450,9 +426,11 @@ class AsyncHttpx:
|
||||
return res.content
|
||||
|
||||
cache_key = url
|
||||
if cached := await cls._content_cache.get(cache_key):
|
||||
return cached
|
||||
|
||||
async with cls._content_cache_lock:
|
||||
cached = cls._get_cached_content_nolock(cache_key)
|
||||
if cached is not None:
|
||||
if cached := await cls._content_cache.get(cache_key):
|
||||
return cached
|
||||
task = cls._content_inflight.get(cache_key)
|
||||
if task is None:
|
||||
|
||||
@@ -1,25 +1,77 @@
|
||||
from collections import OrderedDict
|
||||
import time
|
||||
from typing import ClassVar
|
||||
|
||||
|
||||
class MessageManager:
|
||||
data: ClassVar[dict[str, list[str]]] = {}
|
||||
_MAX_USERS: ClassVar[int] = 4096
|
||||
_MAX_MESSAGES_PER_USER: ClassVar[int] = 200
|
||||
_TRIM_MESSAGES_TO: ClassVar[int] = 100
|
||||
_USER_TTL_SECONDS: ClassVar[float] = 6 * 60 * 60
|
||||
data: ClassVar[OrderedDict[str, tuple[float, list[str]]]] = OrderedDict()
|
||||
|
||||
@classmethod
|
||||
def _prune(cls, now: float | None = None) -> None:
|
||||
now = time.monotonic() if now is None else now
|
||||
stale_before = now - cls._USER_TTL_SECONDS
|
||||
stale_uids = [
|
||||
uid for uid, (last_seen, _) in cls.data.items() if last_seen <= stale_before
|
||||
]
|
||||
for uid in stale_uids:
|
||||
cls.data.pop(uid, None)
|
||||
while len(cls.data) > cls._MAX_USERS:
|
||||
cls.data.popitem(last=False)
|
||||
|
||||
@classmethod
|
||||
def _touch(cls, uid: str, messages: list[str], now: float | None = None) -> None:
|
||||
now = time.monotonic() if now is None else now
|
||||
cls.data[uid] = (now, messages)
|
||||
cls.data.move_to_end(uid)
|
||||
|
||||
@classmethod
|
||||
def add(cls, uid: str, msg_id: str):
|
||||
if uid not in cls.data:
|
||||
cls.data[uid] = []
|
||||
cls.data[uid].append(msg_id)
|
||||
now = time.monotonic()
|
||||
cls._prune(now)
|
||||
_, messages = cls.data.get(uid, (now, []))
|
||||
messages.append(msg_id)
|
||||
cls._touch(uid, messages, now)
|
||||
cls.remove_check(uid)
|
||||
cls._prune(now)
|
||||
|
||||
@classmethod
|
||||
def check(cls, uid: str, msg_id: str) -> bool:
|
||||
return msg_id in cls.data.get(uid, [])
|
||||
now = time.monotonic()
|
||||
cls._prune(now)
|
||||
entry = cls.data.get(uid)
|
||||
if entry is None:
|
||||
return False
|
||||
_, messages = entry
|
||||
cls._touch(uid, messages, now)
|
||||
return msg_id in messages
|
||||
|
||||
@classmethod
|
||||
def remove_check(cls, uid: str):
|
||||
if len(cls.data[uid]) > 200:
|
||||
cls.data[uid] = cls.data[uid][100:]
|
||||
entry = cls.data.get(uid)
|
||||
if entry is None:
|
||||
return
|
||||
_, messages = entry
|
||||
if len(messages) > cls._MAX_MESSAGES_PER_USER:
|
||||
messages = messages[-cls._TRIM_MESSAGES_TO :]
|
||||
cls._touch(uid, messages)
|
||||
|
||||
@classmethod
|
||||
def get(cls, uid: str) -> list[str]:
|
||||
return cls.data[uid] if uid in cls.data else []
|
||||
now = time.monotonic()
|
||||
cls._prune(now)
|
||||
entry = cls.data.get(uid)
|
||||
if entry is None:
|
||||
return []
|
||||
_, messages = entry
|
||||
cls._touch(uid, messages, now)
|
||||
return list(messages)
|
||||
|
||||
@classmethod
|
||||
def clear_all(cls) -> int:
|
||||
size = len(cls.data)
|
||||
cls.data.clear()
|
||||
return size
|
||||
|
||||
@@ -8,6 +8,7 @@ from zhenxun.configs.config import Config
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
LOG_COMMAND = "VirtualEnvPackageManager"
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
|
||||
Config.add_plugin_config(
|
||||
"virtualenv",
|
||||
@@ -191,6 +192,46 @@ class VirtualEnvPackageManager:
|
||||
)
|
||||
return stderr
|
||||
|
||||
@classmethod
|
||||
async def add_requirement(cls, requirement_file: Path):
|
||||
"""将依赖文件写入项目依赖并同步环境
|
||||
|
||||
插件商店安装依赖需要持久化到 pyproject.toml/uv.lock,避免重建环境后丢失。
|
||||
"""
|
||||
if not requirement_file.exists():
|
||||
raise FileNotFoundError(f"依赖文件 {requirement_file} 不存在", LOG_COMMAND)
|
||||
cls._clean_requirements_file(requirement_file)
|
||||
try:
|
||||
command = [
|
||||
"uv",
|
||||
"add",
|
||||
"--requirements",
|
||||
str(requirement_file.absolute()),
|
||||
]
|
||||
logger.info(f"执行项目依赖添加指令: {command}", LOG_COMMAND)
|
||||
result = await asyncio.to_thread(
|
||||
subprocess.run,
|
||||
command,
|
||||
cwd=PROJECT_ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
logger.debug(
|
||||
f"项目依赖添加指令执行完成: {result.stdout}",
|
||||
LOG_COMMAND,
|
||||
)
|
||||
return result.stdout
|
||||
except (CalledProcessError, FileNotFoundError) as e:
|
||||
stderr = e.stderr if isinstance(e, CalledProcessError) else str(e)
|
||||
logger.error(
|
||||
f"项目依赖添加指令执行失败: {stderr}.",
|
||||
LOG_COMMAND,
|
||||
)
|
||||
return stderr
|
||||
|
||||
@classmethod
|
||||
async def list(cls) -> str:
|
||||
"""列出已安装的依赖包"""
|
||||
|
||||
Reference in New Issue
Block a user