mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-04 11:20:01 +08:00
添加uv支持 (#2119)
* bugfix:修复内存泄露和信号量饥饿问题 * 修复图片渲染按高度截断问题 * 优化图片渲染速度 * 权限检查去掉无效引用代码 * 添加uv支持 * 🚨 auto fix by pre-commit hooks * bugfix:修改gitignore换行 * bugfix:修复测试没有新生成uv.lock * 修复导入错误 * bugfix:移除重复调用 * 🚨 auto fix by pre-commit hooks * 清理残余poetry引用 * 更新uv安装方式 * 修复阿里云获取问题 * 增加资源下载提示 * 🚨 auto fix by pre-commit hooks * 修改资源下载为流式 * 🚨 auto fix by pre-commit hooks * 提高启动速度 * 移除bot.py支持 * 🚨 auto fix by pre-commit hooks * 优化win脚本逻辑 * 🚨 auto fix by pre-commit hooks * 清理残余无效逻辑 * 代码改进 * 🚨 auto fix by pre-commit hooks * 增加数据库迁移存在性检查 * 🚨 auto fix by pre-commit hooks * chore(test): 添加pytest超时控制和优雅关闭机制 - 在GitHub Actions工作流中添加作业级和步骤级超时限制,防止测试无限期挂起 - 添加pytest-timeout依赖并配置全局超时为120秒 - 在send_queue服务添加关闭钩子,确保worker任务正确取消 - 在priority_manager添加on_shutdown钩子,支持优先级生命周期的关闭阶段 * chore(lint): 禁用超长行的lint警告 * Modify restart logic for Windows platform * 🚨 auto fix by pre-commit hooks * bugfix:修复sys导入问题 * 清理无效结构 * bugfix:修复路径问题 * bugfix:修复shell语法传递给git导致资源获取失败问题 * 优化关闭显示 * bugfix:修复路径问题 * bugfix:增加路径安全 * bugfix:修复orm绕过问题 * 放宽numpy版本限制 * 修改重启方案 * bugfix:修复循环导入 * 优化逻辑 * Enhance disconnect function with error handling Added error handling for disconnect function and imported ConfigurationError. * Implement emergency restart mechanism Added emergency restart mechanism using atexit to ensure process restart even on severe exceptions during shutdown. * 🚨 auto fix by pre-commit hooks * 重启行为归一化 * 修复测试检测问题 * bugfix:修复测试侧类型报错问题 * 引入launcher机制 * 移除重启测试 * 收紧缓存调用路径 * 类型注解收敛 * 优化浏览器回收行为 * 优化浏览器渲染 * bugfix:解决重复关闭浏览器问题 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: ManyManyTomato <93612024+ATTomatoo@users.noreply.github.com> Co-authored-by: AkashiCoin <l1040186796@gmail.com>
This commit is contained in:
co-authored by
pre-commit-ci[bot]
ManyManyTomato
AkashiCoin
parent
74bf912d04
commit
8b16126e40
@@ -52,9 +52,7 @@ from .auth.exception import (
|
||||
|
||||
AUTH_HOOKS_CONCURRENCY_LIMIT = 5
|
||||
AUTH_DB_CONCURRENCY_LIMIT = 6
|
||||
AUTH_PLUGIN_CACHE_TTL = 30
|
||||
AUTH_USER_CACHE_TTL = 5
|
||||
AUTH_EVENT_CACHE_TTL = 2
|
||||
AUTH_EVENT_CACHE_TTL = 5 # 增加到5秒,减少缓存抖动
|
||||
|
||||
|
||||
# 超时设置(秒)
|
||||
@@ -75,17 +73,6 @@ CIRCUIT_RESET_TIME = 300 # 5分钟
|
||||
HOOKS_CONCURRENCY_LIMIT = AUTH_HOOKS_CONCURRENCY_LIMIT
|
||||
DB_CONCURRENCY_LIMIT = AUTH_DB_CONCURRENCY_LIMIT
|
||||
|
||||
PLUGIN_CACHE_TTL = AUTH_PLUGIN_CACHE_TTL
|
||||
USER_CACHE_TTL = AUTH_USER_CACHE_TTL
|
||||
|
||||
PLUGIN_CACHE = (
|
||||
CacheDict("AUTH_PLUGIN_CACHE", expire=PLUGIN_CACHE_TTL)
|
||||
if PLUGIN_CACHE_TTL > 0
|
||||
else None
|
||||
)
|
||||
USER_CACHE = (
|
||||
CacheDict("AUTH_USER_CACHE", expire=USER_CACHE_TTL) if USER_CACHE_TTL > 0 else None
|
||||
)
|
||||
EVENT_CACHE_TTL = AUTH_EVENT_CACHE_TTL
|
||||
EVENT_CACHE = (
|
||||
CacheDict("AUTH_EVENT_CACHE", expire=EVENT_CACHE_TTL)
|
||||
@@ -110,14 +97,12 @@ HEAVY_COMMAND_MODULES = frozenset({"shop", "sign_in"})
|
||||
|
||||
# 全局信号量与计数器
|
||||
HOOKS_ACTIVE_COUNT = 0
|
||||
HOOKS_ACTIVE_LOCK = asyncio.Lock()
|
||||
HOOKS_SEMAPHORE = asyncio.Semaphore(HOOKS_CONCURRENCY_LIMIT)
|
||||
COMMAND_MATCHER_SEMAPHORE = asyncio.Semaphore(COMMAND_MATCHER_CONCURRENCY)
|
||||
HEAVY_COMMAND_SEMAPHORE = asyncio.Semaphore(HEAVY_COMMAND_CONCURRENCY)
|
||||
|
||||
DB_SEMAPHORE = asyncio.Semaphore(DB_CONCURRENCY_LIMIT)
|
||||
DB_ACTIVE_COUNT = 0
|
||||
DB_ACTIVE_LOCK = asyncio.Lock()
|
||||
_CHECK_MATCHER_PATCHED = False
|
||||
_ORIGINAL_CHECK_AND_RUN_MATCHER: Callable[..., Awaitable[None]] | None = None
|
||||
_MATCHER_COMMAND_TYPE_CACHE: dict[type[Matcher], bool] = {}
|
||||
@@ -171,20 +156,6 @@ class HookTraceRecorder:
|
||||
return self._data if self._enabled else {}
|
||||
|
||||
|
||||
def _cache_get(cache: CacheDict | None, key: str):
|
||||
if not cache:
|
||||
return None
|
||||
try:
|
||||
return cache[key]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def _cache_set(cache: CacheDict | None, key: str, value):
|
||||
if cache:
|
||||
cache[key] = value
|
||||
|
||||
|
||||
def _debug_log(message: str, *args, **kwargs) -> None:
|
||||
if is_overloaded():
|
||||
return
|
||||
@@ -323,6 +294,9 @@ async def _ensure_route_index():
|
||||
continue
|
||||
module = plugin.name
|
||||
_ROUTE_MODULES_WITH_COMMANDS.add(module)
|
||||
module_name = getattr(plugin, "module_name", None) or ""
|
||||
if module_name and module_name != module:
|
||||
_ROUTE_MODULES_WITH_COMMANDS.add(module_name)
|
||||
for normalized in command_set:
|
||||
_ROUTE_COMMAND_MAP.setdefault(normalized, set()).add(module)
|
||||
_ROUTE_PREFIX_MAP.setdefault(normalized[0], set()).add(normalized)
|
||||
@@ -488,6 +462,12 @@ def _matcher_route_cache_key(event: Event) -> str:
|
||||
|
||||
def _event_plain_text(event: Event) -> str:
|
||||
with contextlib.suppress(Exception):
|
||||
# Use raw_message if available (OneBot v11) to get the original text
|
||||
# before nickname stripping. This ensures command matching works correctly
|
||||
# for commands like "真寻日报" when "真寻" is a bot nickname.
|
||||
raw = getattr(event, "raw_message", None)
|
||||
if isinstance(raw, str) and raw:
|
||||
return raw.strip()
|
||||
return (event.get_plaintext() or "").strip()
|
||||
return ""
|
||||
|
||||
@@ -721,6 +701,10 @@ async def _check_matcher_prefilter(
|
||||
return False, None
|
||||
|
||||
|
||||
_MATCHER_SEMAPHORE_TIMEOUT = 8.0
|
||||
_MAX_MATCHER_CACHE = 512
|
||||
|
||||
|
||||
async def _patched_check_and_run_matcher(
|
||||
Matcher: type[Matcher],
|
||||
bot: Bot,
|
||||
@@ -749,12 +733,25 @@ async def _patched_check_and_run_matcher(
|
||||
}
|
||||
if _is_command_matcher_class(Matcher):
|
||||
module = _matcher_module_name(Matcher)
|
||||
if _is_heavy_command_module(module):
|
||||
async with HEAVY_COMMAND_SEMAPHORE:
|
||||
await original(**kwargs)
|
||||
return
|
||||
async with COMMAND_MATCHER_SEMAPHORE:
|
||||
sem = (
|
||||
HEAVY_COMMAND_SEMAPHORE
|
||||
if _is_heavy_command_module(module)
|
||||
else COMMAND_MATCHER_SEMAPHORE
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(sem.acquire(), timeout=_MATCHER_SEMAPHORE_TIMEOUT)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(
|
||||
f"matcher semaphore acquire timeout for {module}, "
|
||||
"executing without concurrency limit",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
await original(**kwargs)
|
||||
return
|
||||
try:
|
||||
await original(**kwargs)
|
||||
finally:
|
||||
sem.release()
|
||||
return
|
||||
await original(**kwargs)
|
||||
|
||||
@@ -820,6 +817,13 @@ async def _cache_sweep_loop() -> None:
|
||||
if EVENT_CACHE is not None:
|
||||
_ = len(EVENT_CACHE)
|
||||
_ = len(_CHECK_MATCHER_ROUTE_CACHE)
|
||||
for _mc in (
|
||||
_MATCHER_COMMAND_TYPE_CACHE,
|
||||
_MATCHER_COMMAND_LITERAL_CACHE,
|
||||
_MATCHER_ALCONNA_SHORTCUT_CACHE,
|
||||
):
|
||||
if len(_mc) > _MAX_MATCHER_CACHE:
|
||||
_mc.clear()
|
||||
|
||||
|
||||
async def start_auth_runtime_tasks() -> None:
|
||||
@@ -857,19 +861,13 @@ async def _has_limits_cached(module: str, event_cache: dict | None) -> bool:
|
||||
async def _db_section():
|
||||
global DB_ACTIVE_COUNT
|
||||
await DB_SEMAPHORE.acquire()
|
||||
async with DB_ACTIVE_LOCK:
|
||||
DB_ACTIVE_COUNT += 1
|
||||
_debug_log(f"current db auth concurrency: {DB_ACTIVE_COUNT}", LOGGER_COMMAND)
|
||||
DB_ACTIVE_COUNT += 1
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
DB_SEMAPHORE.release()
|
||||
async with DB_ACTIVE_LOCK:
|
||||
DB_ACTIVE_COUNT = max(DB_ACTIVE_COUNT - 1, 0)
|
||||
_debug_log(
|
||||
f"current db auth concurrency: {DB_ACTIVE_COUNT}", LOGGER_COMMAND
|
||||
)
|
||||
DB_ACTIVE_COUNT = max(DB_ACTIVE_COUNT - 1, 0)
|
||||
|
||||
|
||||
async def _get_group_cached(entity, event_cache) -> GroupSnapshot | None:
|
||||
@@ -1167,16 +1165,15 @@ async def time_hook(coro, name, recorder: HookTraceRecorder | None = None):
|
||||
async def _enter_hooks_section():
|
||||
"""尝试获取全局信号量并更新计数器,超时则抛出 PermissionExemption。"""
|
||||
global HOOKS_ACTIVE_COUNT
|
||||
await HOOKS_SEMAPHORE.acquire()
|
||||
async with HOOKS_ACTIVE_LOCK:
|
||||
HOOKS_ACTIVE_COUNT += 1
|
||||
_debug_log(
|
||||
(
|
||||
"当前并发权限检查数量: "
|
||||
f"{HOOKS_ACTIVE_COUNT}, limit={HOOKS_CONCURRENCY_LIMIT}"
|
||||
),
|
||||
try:
|
||||
await asyncio.wait_for(HOOKS_SEMAPHORE.acquire(), timeout=TIMEOUT_SECONDS)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(
|
||||
"hooks semaphore acquire timeout, allowing pass",
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
raise PermissionExemption("hooks semaphore timeout, allow pass")
|
||||
HOOKS_ACTIVE_COUNT += 1
|
||||
|
||||
|
||||
async def _leave_hooks_section():
|
||||
@@ -1184,15 +1181,7 @@ async def _leave_hooks_section():
|
||||
global HOOKS_ACTIVE_COUNT
|
||||
with contextlib.suppress(Exception):
|
||||
HOOKS_SEMAPHORE.release()
|
||||
async with HOOKS_ACTIVE_LOCK:
|
||||
HOOKS_ACTIVE_COUNT = max(HOOKS_ACTIVE_COUNT - 1, 0)
|
||||
_debug_log(
|
||||
(
|
||||
"当前并发权限检查数量: "
|
||||
f"{HOOKS_ACTIVE_COUNT}, limit={HOOKS_CONCURRENCY_LIMIT}"
|
||||
),
|
||||
LOGGER_COMMAND,
|
||||
)
|
||||
HOOKS_ACTIVE_COUNT = max(HOOKS_ACTIVE_COUNT - 1, 0)
|
||||
|
||||
|
||||
async def auth_ban_fast(
|
||||
@@ -1283,6 +1272,12 @@ async def auth_precheck(
|
||||
await LevelUserMemoryCache.ensure_fresh()
|
||||
levels = await LevelUserMemoryCache.get_levels(session.user.id, entity.group_id)
|
||||
await auth_admin(plugin, session, cached_levels=levels)
|
||||
# 缓存 admin 检查结果到 event_cache,避免 auth() 重复执行
|
||||
event_cache = _get_event_cache(event, session, entity)
|
||||
if event_cache is not None:
|
||||
event_cache["admin_levels"] = levels
|
||||
event_cache["admin_timeout"] = False
|
||||
event_cache["admin_precheck_done"] = True
|
||||
|
||||
|
||||
async def _call_auth_ban_compat(
|
||||
@@ -1421,51 +1416,39 @@ async def auth(
|
||||
elif plugin.plugin_type == PluginType.SUPERUSER:
|
||||
raise SkipPluginException("超级管理员权限不足...")
|
||||
if not admin_checked_pre:
|
||||
await LevelUserMemoryCache.ensure_fresh()
|
||||
admin_levels = None
|
||||
admin_timeout = False
|
||||
if event_cache is not None:
|
||||
admin_levels, admin_timeout = await _get_admin_levels_cached(
|
||||
session, entity, event_cache
|
||||
)
|
||||
if admin_timeout:
|
||||
hook_recorder.set("auth_admin", "timeout")
|
||||
if event_cache is not None and event_cache.get("admin_precheck_done"):
|
||||
hook_recorder.set("auth_admin", "precheck")
|
||||
admin_checked_pre = True
|
||||
else:
|
||||
admin_start = time.time()
|
||||
await auth_admin(plugin, session, cached_levels=admin_levels)
|
||||
hook_recorder.set(
|
||||
"auth_admin", f"{time.time() - admin_start:.3f}s(pre)"
|
||||
)
|
||||
await LevelUserMemoryCache.ensure_fresh()
|
||||
admin_levels = None
|
||||
admin_timeout = False
|
||||
if event_cache is not None:
|
||||
admin_levels, admin_timeout = await _get_admin_levels_cached(
|
||||
session, entity, event_cache
|
||||
)
|
||||
if admin_timeout:
|
||||
hook_recorder.set("auth_admin", "timeout")
|
||||
else:
|
||||
admin_start = time.time()
|
||||
await auth_admin(plugin, session, cached_levels=admin_levels)
|
||||
hook_recorder.set(
|
||||
"auth_admin", f"{time.time() - admin_start:.3f}s(pre)"
|
||||
)
|
||||
admin_checked_pre = True
|
||||
|
||||
ban_cache_state = None
|
||||
if event_cache is not None:
|
||||
ban_cache_state = event_cache.get("ban_state")
|
||||
if skip_ban:
|
||||
if ban_cache_state is True:
|
||||
hook_recorder.set("auth_ban", "cached")
|
||||
raise SkipPluginException("user or group banned (cached)")
|
||||
if ban_cache_state is None:
|
||||
ban_start = time.time()
|
||||
try:
|
||||
await _call_auth_ban_compat(
|
||||
matcher, bot, session, plugin, entity=entity
|
||||
)
|
||||
hook_recorder.set("auth_ban", f"{time.time() - ban_start:.3f}s")
|
||||
if event_cache is not None:
|
||||
event_cache["ban_state"] = False
|
||||
except SkipPluginException:
|
||||
hook_recorder.set("auth_ban", f"{time.time() - ban_start:.3f}s")
|
||||
if event_cache is not None:
|
||||
event_cache["ban_state"] = True
|
||||
raise
|
||||
else:
|
||||
if ban_cache_state is True:
|
||||
hook_recorder.set("auth_ban", "cached")
|
||||
raise SkipPluginException("user or group banned (cached)")
|
||||
if ban_cache_state is False:
|
||||
hook_recorder.set("auth_ban", "cached")
|
||||
elif ban_cache_state is None:
|
||||
if skip_ban:
|
||||
hook_recorder.set("auth_ban", "skipped")
|
||||
else:
|
||||
if ban_cache_state is True:
|
||||
hook_recorder.set("auth_ban", "cached")
|
||||
raise SkipPluginException("user or group banned (cached)")
|
||||
if ban_cache_state is None:
|
||||
else:
|
||||
ban_start = time.time()
|
||||
try:
|
||||
await _call_auth_ban_compat(
|
||||
@@ -1479,8 +1462,6 @@ async def auth(
|
||||
if event_cache is not None:
|
||||
event_cache["ban_state"] = True
|
||||
raise
|
||||
else:
|
||||
hook_recorder.set("auth_ban", "cached")
|
||||
|
||||
# 获取插件费用
|
||||
if not route_skip_checks and plugin.cost_gold > 0:
|
||||
|
||||
@@ -174,6 +174,11 @@ async def _auth_preprocessor(
|
||||
):
|
||||
if event.get_type() == "message" and not is_cache_ready():
|
||||
raise IgnoredException("cache not ready ignore")
|
||||
|
||||
# 提前判断是否跳过权限检查
|
||||
if _skip_auth_for_plugin(matcher):
|
||||
return
|
||||
|
||||
start_time = time.time()
|
||||
entity = state.get("_zx_entity")
|
||||
if entity is None:
|
||||
@@ -216,8 +221,6 @@ async def _auth_preprocessor(
|
||||
route_modules=route_modules,
|
||||
):
|
||||
return
|
||||
if _skip_auth_for_plugin(matcher):
|
||||
return
|
||||
|
||||
try:
|
||||
await auth(
|
||||
|
||||
@@ -69,21 +69,25 @@ _blmt = BanCheckLimiter(
|
||||
async def _(
|
||||
matcher: Matcher, bot: Bot, session: EventSession, state: T_State, event: Event
|
||||
):
|
||||
module = None
|
||||
if plugin := matcher.plugin:
|
||||
module = plugin.module_name
|
||||
if not (metadata := plugin.metadata):
|
||||
return
|
||||
extra = metadata.extra
|
||||
if extra.get("plugin_type") in [
|
||||
PluginType.HIDDEN,
|
||||
PluginType.DEPENDANT,
|
||||
PluginType.ADMIN,
|
||||
PluginType.SUPERUSER,
|
||||
]:
|
||||
return
|
||||
# 提前判断 notice 类型,直接跳过
|
||||
if matcher.type == "notice":
|
||||
return
|
||||
|
||||
# 提前判断插件类型,跳过不需要检测的插件
|
||||
if plugin := matcher.plugin:
|
||||
if metadata := plugin.metadata:
|
||||
extra = metadata.extra
|
||||
if extra.get("plugin_type") in [
|
||||
PluginType.HIDDEN,
|
||||
PluginType.DEPENDANT,
|
||||
PluginType.ADMIN,
|
||||
PluginType.SUPERUSER,
|
||||
]:
|
||||
return
|
||||
module = plugin.module_name
|
||||
else:
|
||||
return
|
||||
|
||||
user_id = session.id1
|
||||
group_id = session.id3 or session.id2
|
||||
malicious_ban_time = Config.get_config("hook", "MALICIOUS_BAN_TIME")
|
||||
|
||||
@@ -13,6 +13,8 @@ async def _(
|
||||
exception: Exception | None,
|
||||
bot: Bot,
|
||||
):
|
||||
if not WithdrawManager._data:
|
||||
return
|
||||
tasks = []
|
||||
index_list = list(WithdrawManager._data.keys())
|
||||
for index in index_list:
|
||||
|
||||
Reference in New Issue
Block a user