bugfix:修复sqlite部分场景下锁竞争问题;新增插件恶意触发配置文件 (#2144)

* bugfix:修复sqlite部分场景下锁竞争问题;新增插件恶意触发配置文件

* 文件没同步完
This commit is contained in:
Copaan
2026-06-24 09:11:03 +08:00
committed by GitHub
parent 73cbe2a609
commit f4d2342693
39 changed files with 1577 additions and 829 deletions
@@ -1,10 +1,6 @@
import asyncio
import time
from nonebot import on_message
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import UniMsg
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import Config
@@ -12,7 +8,12 @@ from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.models.chat_history import ChatHistory
from zhenxun.services.db_context import with_db_timeout
from zhenxun.services.log import logger
from zhenxun.services.message_load import is_overloaded, should_pause_tasks
from zhenxun.services.low_priority_writer import (
LowPriorityWriterConfig,
append_low_priority_record,
register_low_priority_writer,
)
from zhenxun.services.message_load import is_overloaded
from zhenxun.utils.enum import PluginType
from zhenxun.utils.utils import get_entity_ids
@@ -44,23 +45,45 @@ def rule(message: UniMsg) -> bool:
chat_history = on_message(rule=rule, priority=1, block=False)
_HISTORY_QUEUE: asyncio.Queue[ChatHistory] = asyncio.Queue(maxsize=5000)
_DROP_COUNT = 0
_LAST_DROP_LOG = 0.0
_DROP_LOG_INTERVAL = 10.0
_WRITER_NAME = "chat_history"
_FLUSH_BATCH_SIZE = 200
_FLUSH_MAX_PER_TICK = 1000
_FLUSH_DB_TIMEOUT = 5.0
async def _write_chat_history_batch(batch: list[ChatHistory], reason: str) -> None:
await with_db_timeout(
ChatHistory.bulk_create(batch, _FLUSH_BATCH_SIZE),
timeout=_FLUSH_DB_TIMEOUT,
operation=f"ChatHistory.bulk_create[{len(batch)}]",
source=f"chat_history:{reason}",
)
register_low_priority_writer(
LowPriorityWriterConfig(
name=_WRITER_NAME,
write_batch=_write_chat_history_batch,
batch_size=_FLUSH_BATCH_SIZE,
trigger_size=_FLUSH_BATCH_SIZE,
max_retain=5000,
flush_interval_seconds=60.0,
max_items_per_cycle=_FLUSH_MAX_PER_TICK,
backoff_base_seconds=30.0,
backoff_max_seconds=600.0,
log_command="chat_history",
)
)
@chat_history.handle()
async def _(message: UniMsg, session: Uninfo):
entity = get_entity_ids(session)
now = time.time()
if is_overloaded():
return
try:
_HISTORY_QUEUE.put_nowait(
await append_low_priority_record(
_WRITER_NAME,
ChatHistory(
user_id=entity.user_id,
group_id=entity.group_id,
@@ -68,50 +91,7 @@ async def _(message: UniMsg, session: Uninfo):
plain_text=message.extract_plain_text(),
bot_id=session.self_id,
platform=session.platform,
)
),
)
except asyncio.QueueFull:
global _DROP_COUNT, _LAST_DROP_LOG
_DROP_COUNT += 1
if now - _LAST_DROP_LOG > _DROP_LOG_INTERVAL:
_LAST_DROP_LOG = now
logger.debug(
f"chat_history queue full, dropped {_DROP_COUNT} items",
"chat_history",
)
@scheduler.scheduled_job(
"interval",
minutes=1,
max_instances=1,
coalesce=True,
)
async def _():
try:
if should_pause_tasks():
return
flushed = 0
while flushed < _FLUSH_MAX_PER_TICK:
message_list: list[ChatHistory] = []
limit = min(_FLUSH_BATCH_SIZE, _FLUSH_MAX_PER_TICK - flushed)
for _ in range(limit):
try:
message_list.append(_HISTORY_QUEUE.get_nowait())
except asyncio.QueueEmpty:
break
if not message_list:
break
await with_db_timeout(
ChatHistory.bulk_create(message_list, _FLUSH_BATCH_SIZE),
timeout=_FLUSH_DB_TIMEOUT,
operation=f"ChatHistory.bulk_create[{len(message_list)}]",
source="chat_history",
)
flushed += len(message_list)
if flushed:
backlog = _HISTORY_QUEUE.qsize()
suffix = f",剩余队列 {backlog} 条" if backlog else ""
logger.debug(f"批量添加聊天记录 {flushed} 条{suffix}", "定时任务")
except Exception as e:
logger.warning("存储聊天记录失败", "chat_history", e=e)
+74 -24
View File
@@ -15,7 +15,9 @@ from zhenxun.services import (
avatar_service,
generate,
)
from zhenxun.services.db_context import with_db_timeout
from zhenxun.services.log import logger
from zhenxun.services.message_load import is_db_unhealthy
from zhenxun.services.renderer.result_cache import RenderResultMemoryCache
from zhenxun.ui.models import PluginMenuCategory, PluginMenuData
from zhenxun.utils.common_utils import format_usage_for_markdown
@@ -25,6 +27,8 @@ from zhenxun.utils.platform import PlatformUtils
from .utils import classify_plugin
driver = nonebot.get_driver()
_DB_BUSY_MESSAGE = "数据库繁忙,请稍后再试"
_HELP_DB_TIMEOUT = 3.0
_HELP_MENU_IMAGE_CACHE = RenderResultMemoryCache(
ttl_seconds=300,
max_items=64,
@@ -32,6 +36,24 @@ _HELP_MENU_IMAGE_CACHE = RenderResultMemoryCache(
)
class _DbBusyError(Exception):
pass
async def _read_db(factory, operation: str):
if is_db_unhealthy():
raise _DbBusyError
try:
return await with_db_timeout(
factory(),
timeout=_HELP_DB_TIMEOUT,
operation=operation,
source="help",
)
except TimeoutError as exc:
raise _DbBusyError from exc
def _create_plugin_menu_item(
bot: BotConsole | None,
plugin: PluginInfo,
@@ -79,11 +101,17 @@ def _create_plugin_menu_item(
async def create_help_img(
session: Uninfo, group_id: str | None, is_detail: bool
) -> bytes:
) -> str | bytes:
"""使用渲染服务生成帮助图片"""
classified_data = await classify_plugin(
session, group_id, is_detail, _create_plugin_menu_item
)
try:
classified_data = await _read_db(
lambda: classify_plugin(
session, group_id, is_detail, _create_plugin_menu_item
),
"Help.classify_plugin",
)
except _DbBusyError:
return _DB_BUSY_MESSAGE
sorted_categories = dict(
sorted(classified_data.items(), key=lambda x: len(x[1]), reverse=True)
@@ -157,9 +185,11 @@ async def get_user_allow_help(user_id: str) -> list[str]:
list[str]: 插件类型列表
"""
type_list = ["NORMAL", "DEPENDANT"]
for level in await LevelUser.filter(user_id=user_id).values_list(
"user_level", flat=True
):
levels = await _read_db(
lambda: LevelUser.filter(user_id=user_id).values_list("user_level", flat=True),
"Help.user_allow_level",
)
for level in levels:
if level > 0: # type: ignore
type_list.extend(("ADMIN", "ADMIN_SUPER"))
break
@@ -170,7 +200,7 @@ async def get_user_allow_help(user_id: str) -> list[str]:
async def get_plugin_help(
user_id: str, name: str, is_superuser: bool, variant: str | None = None
) -> bytes | None:
) -> str | bytes | None:
"""获取功能的帮助信息
参数:
@@ -179,20 +209,35 @@ async def get_plugin_help(
is_superuser: 是否为超级用户
variant: 使用的皮肤/变体名称
"""
type_list = await get_user_allow_help(user_id)
if name.isdigit():
plugin = await PluginInfo.get_or_none(id=int(name), plugin_type__in=type_list)
else:
plugin = await PluginInfo.get_or_none(
name__iexact=name, load_status=True, plugin_type__in=type_list
)
try:
type_list = await get_user_allow_help(user_id)
if name.isdigit():
plugin = await _read_db(
lambda: PluginInfo.get_or_none(id=int(name), plugin_type__in=type_list),
"Help.plugin_by_id",
)
else:
plugin = await _read_db(
lambda: PluginInfo.get_or_none(
name__iexact=name, load_status=True, plugin_type__in=type_list
),
"Help.plugin_by_name",
)
except _DbBusyError:
return _DB_BUSY_MESSAGE
if plugin:
_plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
if _plugin and _plugin.metadata:
extra_data = PluginExtraData(**_plugin.metadata.extra)
call_count = await Statistics.filter(plugin_name=plugin.module).count()
try:
call_count = await _read_db(
lambda: Statistics.filter(plugin_name=plugin.module).count(),
"Help.plugin_call_count",
)
except _DbBusyError:
return _DB_BUSY_MESSAGE
usage = _plugin.metadata.usage
metadata_items = [
@@ -263,14 +308,19 @@ async def get_llm_help(question: str, user_id: str) -> str | bytes:
"""
try:
allowed_types = await get_user_allow_help(user_id)
plugins = await PluginInfo.get_plugins(
load_status=None,
filter_parent=False,
is_show=True,
plugin_type__in=allowed_types,
)
try:
allowed_types = await get_user_allow_help(user_id)
plugins = await _read_db(
lambda: PluginInfo.get_plugins(
load_status=None,
filter_parent=False,
is_show=True,
plugin_type__in=allowed_types,
),
"Help.llm_plugin_list",
)
except _DbBusyError:
return _DB_BUSY_MESSAGE
knowledge_base_parts = []
for p in plugins:
+18
View File
@@ -40,6 +40,24 @@ Config.add_plugin_config(
type=int,
)
Config.add_plugin_config(
"hook",
"MALICIOUS_CHECK_MODE",
"off",
help="恶意触发检测模式:off=关闭,blacklist=仅列表插件检测,whitelist=列表插件跳过检测",
default_value="off",
type=str,
)
Config.add_plugin_config(
"hook",
"MALICIOUS_CHECK_PLUGINS",
[],
help="恶意触发检测插件列表,按模式作为黑名单或白名单使用,填插件模块名",
default_value=[],
type=list,
)
Config.add_plugin_config(
"hook",
"IS_SEND_TIP_MESSAGE",
+15 -2
View File
@@ -17,7 +17,7 @@ from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.user_console import UserConsole
from zhenxun.services.cache.cache_containers import CacheDict
from zhenxun.services.log import logger
from zhenxun.services.message_load import signal_overload
from zhenxun.services.message_load import is_db_unhealthy, signal_overload
from zhenxun.utils.enum import GoldHandle, PluginType
from zhenxun.utils.exception import InsufficientGold
from zhenxun.utils.platform import PlatformUtils
@@ -811,6 +811,8 @@ async def _run_selected_matcher(
dependency_cache,
lane: str = "command_exact",
) -> None:
# state is copied per matcher before dispatch; keep lane matcher-local.
state["_zx_dispatch_lane"] = lane
async with _dispatch_lane_section(lane):
await nb_message.check_and_run_matcher(
matcher,
@@ -938,6 +940,11 @@ async def _has_limits_cached(
event_cache.setdefault("module_limit_entries", {})[module] = limit_entries
event_cache.setdefault("module_limits_ready", {})[module] = True
return has_limits
if is_db_unhealthy():
module_limit_cache[module] = False
if event_cache is not None:
event_cache.setdefault("module_limits_ready", {})[module] = False
return False
limits = await LimitManager.get_module_limits(module)
has_limits = bool(limits)
module_limit_cache[module] = has_limits
@@ -1075,7 +1082,7 @@ async def _get_plugin_cache_first(
plugin = provider.get_plugin_if_ready(module)
cache_miss = plugin is None and not provider.plugin_cache_loaded()
if plugin is None and allow_cache_load:
if plugin is None and allow_cache_load and not is_db_unhealthy():
plugin = await provider.get_plugin(module)
cache_miss = False
if event_cache is not None:
@@ -1324,6 +1331,9 @@ async def _prepare_auth_state_with_fallback(
)
if prep is not None:
return prep
if is_db_unhealthy():
hook_recorder.set("auth_snapshot", "cache_miss_db_unhealthy")
return None
hook_recorder.set("auth_snapshot", "cache_miss_fallback")
return await _prepare_auth_state(
module=module,
@@ -1408,6 +1418,9 @@ async def _resolve_cost_gold(
if prep.profile.cost_gold <= 0:
hook_recorder.set("cost_gold", "skipped")
return 0
if is_db_unhealthy():
hook_recorder.set("cost_gold", "db_unhealthy")
raise SkipPluginException("数据库繁忙,金币功能暂不可用...")
cost_start = time.time()
try:
if prep.user is None:
@@ -39,6 +39,49 @@ class HandleEventSelectorDependencies:
_HANDLE_EVENT_PATCHED = False
_ORIGINAL_HANDLE_EVENT: Callable[..., Awaitable[None]] | None = None
_ORIGINAL_ADAPTER_HANDLE_EVENTS: dict[object, object] = {}
_MATCHER_DEADLINE_BY_LANE = {
"system": 15.0,
"command": 12.0,
"temp": 15.0,
"passive_light": 3.0,
"fallback_ai": 5.0,
}
_DEFAULT_MATCHER_DEADLINE = 5.0
def _matcher_deadline_for_lane(lane: str) -> float:
return _MATCHER_DEADLINE_BY_LANE.get(lane, _DEFAULT_MATCHER_DEADLINE)
def _matcher_name(matcher: type[Matcher]) -> str:
module = str(getattr(matcher, "module", "") or "")
lineno = str(getattr(matcher, "lineno", "") or "")
matcher_type = str(getattr(matcher, "type", "") or "")
name = module or matcher.__name__
if lineno:
name = f"{name}:{lineno}"
if matcher_type:
name = f"{name}<{matcher_type}>"
return name
async def _run_matcher_with_deadline(
anyio_mod: Any,
coro: Awaitable[None],
matcher: type[Matcher],
lane: str,
) -> None:
timeout = _matcher_deadline_for_lane(lane)
try:
with anyio_mod.fail_after(timeout):
await coro
except TimeoutError:
signal_overload(20.0)
logger.warning(
"matcher dispatch timeout: "
f"matcher={_matcher_name(matcher)}, lane={lane}, timeout={timeout:.1f}s",
LOGGER_COMMAND,
)
def _trim_leading_text(message: Any) -> None:
@@ -140,7 +183,6 @@ async def patched_handle_event(
stop_propagation = getattr(nb_message, "StopPropagation")
handle_exception = getattr(nb_message, "_handle_exception")
anyio_mod = getattr(nb_message, "anyio")
run_coro_with_shield = getattr(nb_message, "run_coro_with_shield")
async with async_exit_stack() as stack:
if not await apply_event_preprocessors(
@@ -256,7 +298,8 @@ async def patched_handle_event(
continue
matcher_state = deps.build_matcher_state(state)
tg.start_soon(
run_coro_with_shield,
_run_matcher_with_deadline,
anyio_mod,
deps.run_selected_matcher(
matcher,
bot,
@@ -266,6 +309,8 @@ async def patched_handle_event(
dependency_cache,
lane,
),
matcher,
lane,
)
if show_log:
@@ -10,6 +10,7 @@ from nonebot.adapters import Bot, Event
from nonebot.matcher import Matcher
from nonebot_plugin_uninfo import Uninfo
from zhenxun.services.message_load import is_db_unhealthy
from zhenxun.utils.utils import EntityIDs
from .auth.context import (
@@ -296,6 +297,9 @@ async def policy_precheck_stage(
ctx.flags = apply_policy_precheck(ctx, deps)
except PermissionExemption as exc:
_recorder(ctx).set("policy_fallback", str(exc))
if is_db_unhealthy():
ctx.stop(allowed=True, effect="allow", reason="db_unhealthy_cache_miss")
return
ctx.prep = await deps.prepare_auth_state(
module=ctx.module,
context=ctx.event_context,
@@ -2,6 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass
from zhenxun.services.message_load import is_db_unhealthy
from zhenxun.utils.enum import BlockType, PluginType
from .auth.data_provider import (
@@ -103,7 +104,7 @@ async def get_plugin_auth_profile(
if limits is None:
limits = provider.get_module_limits_if_ready(module)
limits_ready = limits is not None
if limits is None and allow_cache_load:
if limits is None and allow_cache_load and not is_db_unhealthy():
limits = await provider.get_module_limits(module)
limits_ready = True
if limits is None:
+94 -14
View File
@@ -10,7 +10,9 @@ from zhenxun.services.cache.runtime_cache import (
GroupSnapshot,
LevelUserSnapshot,
)
from zhenxun.services.db_context import with_db_timeout
from zhenxun.services.log import logger
from zhenxun.services.message_load import is_db_unhealthy
from .auth.config import LOGGER_COMMAND
from .auth.context import EventContext
@@ -50,6 +52,41 @@ def _build_runtime_group_snapshot(context: EventContext) -> GroupSnapshot | None
)
def _build_default_bot_snapshot(context: EventContext) -> BotSnapshot:
"""Fail-open bot snapshot used only while DB cold-path is unhealthy."""
return BotSnapshot(
bot_id=context.bot_id,
status=True,
platform=context.platform,
block_plugins="",
block_tasks="",
available_plugins="",
available_tasks="",
)
def _build_default_group_snapshot(context: EventContext) -> GroupSnapshot | None:
"""Fail-open group snapshot used only while DB cold-path is unhealthy."""
if not context.group_id:
return None
return GroupSnapshot(
group_id=context.group_id,
channel_id=context.channel_id,
group_name="",
max_member_count=0,
member_count=0,
status=True,
level=5,
is_super=False,
group_flag=0,
block_plugin="",
superuser_block_plugin="",
block_task="",
superuser_block_task="",
platform=context.platform,
)
def _qq_client_group_repair_key(context: EventContext) -> tuple[str, str] | None:
if context.platform_scope != "qq_client" or not context.group_id:
return None
@@ -72,6 +109,8 @@ async def _repair_missing_qq_client_group(
provider: PermissionDataProvider,
) -> GroupSnapshot | None:
"""Persist a minimal OneBot group when startup group sync returned empty."""
if is_db_unhealthy():
return None
key = _qq_client_group_repair_key(context)
if key is None or not provider.group_cache_loaded():
return None
@@ -97,9 +136,14 @@ async def _repair_missing_qq_client_group(
"group_flag": 1,
"platform": context.platform,
}
group, _ = await GroupConsole.get_or_create_root_group(
group_id=group_id,
defaults=defaults,
group, _ = await with_db_timeout(
GroupConsole.get_or_create_root_group(
group_id=group_id,
defaults=defaults,
),
timeout=2.0,
operation="GroupConsole.get_or_create_root_group",
source="auth_snapshot.repair_missing_group",
)
from zhenxun.services.cache.runtime_cache import GroupMemoryCache
@@ -129,6 +173,7 @@ class AuthSnapshot:
ban_state: bool | None = None
user_balance_loaded: bool = False
user_balance: int | None = None
db_unhealthy: bool = False
cache_misses: frozenset[str] = field(default_factory=frozenset)
@property
@@ -173,43 +218,63 @@ async def build_auth_snapshot(
event_cache = context.event_cache
entity = context.entity
cache_misses: set[str] = set()
db_unhealthy = is_db_unhealthy()
can_load_cache = allow_cache_load and not db_unhealthy
bot_data: BotSnapshot | None = None
if (
event_cache is not None
and "bot_data" in event_cache
and (event_cache.get("bot_cache_ready") or not allow_cache_load)
and (event_cache.get("bot_cache_ready") or not can_load_cache)
):
bot_data = event_cache.get("bot_data")
else:
bot_data = provider.get_bot_if_ready(bot.self_id)
if bot_data is None:
if allow_cache_load:
if can_load_cache:
bot_data = await provider.get_bot(bot.self_id)
elif db_unhealthy:
bot_data = _build_default_bot_snapshot(context)
elif not provider.bot_cache_loaded():
cache_misses.add("bot")
if event_cache is not None:
event_cache["bot_data"] = bot_data
event_cache["bot_cache_ready"] = provider.bot_cache_loaded()
event_cache["bot_cache_ready"] = provider.bot_cache_loaded() or db_unhealthy
if bot_data is None and db_unhealthy:
bot_data = _build_default_bot_snapshot(context)
if event_cache is not None:
event_cache["bot_data"] = bot_data
event_cache["bot_cache_ready"] = True
group = None
if entity.group_id:
if (
event_cache is not None
and "group" in event_cache
and (event_cache.get("group_cache_ready") or not allow_cache_load)
and (event_cache.get("group_cache_ready") or not can_load_cache)
):
group = event_cache.get("group")
else:
group = provider.get_group_if_ready(entity.group_id, entity.channel_id)
if group is None and not provider.group_cache_loaded():
cache_misses.add("group")
elif group is None and allow_cache_load:
elif group is None and can_load_cache:
group = await provider.get_group(entity.group_id, entity.channel_id)
if group is None and db_unhealthy:
group = _build_default_group_snapshot(context)
cache_misses.discard("group")
if event_cache is not None:
event_cache["group"] = group
event_cache["group_cache_ready"] = provider.group_cache_loaded()
if group is None:
event_cache["group_cache_ready"] = (
provider.group_cache_loaded() or db_unhealthy
)
if group is None and db_unhealthy:
group = _build_default_group_snapshot(context)
cache_misses.discard("group")
if event_cache is not None:
event_cache["group"] = group
event_cache["group_cache_ready"] = True
if group is None and not db_unhealthy:
group = await _repair_missing_qq_client_group(
context,
provider=provider,
@@ -232,7 +297,7 @@ async def build_auth_snapshot(
if (
event_cache is not None
and "admin_levels" in event_cache
and (event_cache.get("admin_cache_ready") or not allow_cache_load)
and (event_cache.get("admin_cache_ready") or not can_load_cache)
):
admin_levels = event_cache.get("admin_levels")
else:
@@ -241,16 +306,26 @@ async def build_auth_snapshot(
entity.group_id,
)
if admin_levels is None:
if allow_cache_load:
if can_load_cache:
admin_levels = await provider.get_admin_levels(
entity.user_id,
entity.group_id,
)
elif db_unhealthy:
admin_levels = (None, None)
else:
cache_misses.add("admin_levels")
if event_cache is not None:
event_cache["admin_levels"] = admin_levels
event_cache["admin_cache_ready"] = provider.admin_cache_loaded()
event_cache["admin_cache_ready"] = (
provider.admin_cache_loaded() or db_unhealthy
)
if admin_levels is None and db_unhealthy and not provider.admin_cache_loaded():
admin_levels = (None, None)
cache_misses.discard("admin_levels")
if event_cache is not None:
event_cache["admin_levels"] = admin_levels
event_cache["admin_cache_ready"] = True
ban_state = None
if not skip_ban:
@@ -260,11 +335,15 @@ async def build_auth_snapshot(
ban_state = provider.is_banned(entity.user_id, entity.group_id)
if event_cache is not None:
event_cache["ban_state"] = ban_state
elif allow_cache_load:
elif can_load_cache:
await provider.ensure_ban_loaded()
ban_state = provider.is_banned(entity.user_id, entity.group_id)
if event_cache is not None:
event_cache["ban_state"] = ban_state
elif db_unhealthy:
ban_state = False
if event_cache is not None:
event_cache["ban_state"] = ban_state
else:
cache_misses.add("ban")
@@ -276,6 +355,7 @@ async def build_auth_snapshot(
group=group,
admin_levels=admin_levels,
ban_state=ban_state,
db_unhealthy=db_unhealthy,
cache_misses=frozenset(cache_misses),
)
+121 -28
View File
@@ -61,6 +61,88 @@ _blmt = BanCheckLimiter(
4,
)
_MALICIOUS_CHECK_MODES = {"off", "blacklist", "whitelist"}
_EVENT_PLUGIN_DEDUPE_TTL = 30.0
_EVENT_PLUGIN_DEDUPE_MAX = 4096
_event_plugin_seen: dict[str, float] = {}
def _malicious_check_mode() -> str:
mode = str(Config.get_config("hook", "MALICIOUS_CHECK_MODE") or "off")
mode = mode.strip().lower()
return mode if mode in _MALICIOUS_CHECK_MODES else "off"
def _malicious_plugin_set() -> set[str]:
value = Config.get_config("hook", "MALICIOUS_CHECK_PLUGINS")
if value is None:
return set()
if isinstance(value, str):
items = value.replace("\n", ",").split(",")
elif isinstance(value, list | tuple | set):
items = value
else:
items = [value]
return {str(item).strip().casefold() for item in items if str(item).strip()}
def _should_check_plugin(module: str, lane: str) -> bool:
mode = _malicious_check_mode()
if mode == "off":
return False
normalized_module = str(module or "").strip().casefold()
if not normalized_module:
return False
plugin_set = _malicious_plugin_set()
in_plugin_set = normalized_module in plugin_set
is_passive = str(lane or "").startswith("passive_")
if mode == "blacklist":
return in_plugin_set
if is_passive:
return False
if mode == "whitelist":
return not in_plugin_set
return False
def _event_plugin_key(event: Event, user_id: str, module: str) -> str:
message_id = getattr(event, "message_id", None) or getattr(event, "id", None)
if message_id is None:
message_id = id(event)
return f"{message_id}:{user_id}:{module}"
def _remember_event_plugin_once(key: str) -> bool:
now = time.monotonic()
expires_at = _event_plugin_seen.get(key)
if expires_at is not None and expires_at > now:
return False
_event_plugin_seen[key] = now + _EVENT_PLUGIN_DEDUPE_TTL
if len(_event_plugin_seen) > _EVENT_PLUGIN_DEDUPE_MAX:
target_size = _EVENT_PLUGIN_DEDUPE_MAX // 2
for cache_key, cache_expires_at in list(_event_plugin_seen.items()):
if cache_expires_at <= now or len(_event_plugin_seen) > target_size:
_event_plugin_seen.pop(cache_key, None)
if len(_event_plugin_seen) <= target_size:
break
return True
def _mark_event_plugin_checked(
state: T_State, event: Event, user_id: str, module: str
) -> bool:
checked = state.setdefault("_zx_malicious_checked_plugins", set())
if isinstance(checked, set):
if module in checked:
return False
checked.add(module)
return _remember_event_plugin_once(_event_plugin_key(event, user_id, module))
def _get_positive_config(key: str, cast_type: type[int] | type[float]) -> int | float:
value = Config.get_config("hook", key)
@@ -102,6 +184,10 @@ async def _(
else:
return
lane = state.get("_zx_dispatch_lane")
if not _should_check_plugin(module, lane if isinstance(lane, str) else ""):
return
user_id = resolve_actor_user_id(event, session.id1)
group_id = resolve_event_group_id(event, session.id3 or session.id2)
# 超级用户豁免恶意检测(A6):与权威权限路径保持一致,避免误封管理者。
@@ -111,35 +197,42 @@ async def _(
is_superuser = user_id in bot.config.superusers
if is_superuser:
return
else:
return
if not _mark_event_plugin_checked(state, event, user_id, module):
return
# 只统计通过模式/lane过滤且同事件同插件去重后的有效触发。
limiter_key = f"{user_id}__{module}"
malicious_check_time = float(_get_positive_config("MALICIOUS_CHECK_TIME", float))
malicious_ban_count = int(_get_positive_config("MALICIOUS_BAN_COUNT", int))
malicious_ban_time = int(_get_positive_config("MALICIOUS_BAN_TIME", int))
_blmt.configure(malicious_check_time, malicious_ban_count)
if user_id and module:
if _blmt.check(f"{user_id}__{module}"):
await BanConsole.ban(
user_id,
group_id,
9,
"恶意触发命令检测",
malicious_ban_time * 60,
bot.self_id,
)
logger.info(
f"触发了恶意触发检测: {matcher.plugin_name}",
"HOOK",
session=session,
)
await MessageUtils.build_message(
[
At(flag="user", target=user_id),
"检测到恶意触发命令,您将被封禁 30 分钟",
]
).send()
logger.debug(
f"触发了恶意触发检测: {matcher.plugin_name}",
"HOOK",
session=session,
)
raise IgnoredException("检测到恶意触发命令")
_blmt.add(f"{user_id}__{module}")
if _blmt.check(limiter_key):
await BanConsole.ban(
user_id,
group_id,
9,
"恶意触发命令检测",
malicious_ban_time * 60,
bot.self_id,
)
logger.info(
f"触发了恶意触发检测: {matcher.plugin_name}",
"HOOK",
session=session,
)
await MessageUtils.build_message(
[
At(flag="user", target=user_id),
"检测到恶意触发命令,您将被封禁 30 分钟",
]
).send()
logger.debug(
f"触发了恶意触发检测: {matcher.plugin_name}",
"HOOK",
session=session,
)
raise IgnoredException("检测到恶意触发命令")
_blmt.add(limiter_key)
+53 -12
View File
@@ -12,6 +12,8 @@ from zhenxun.models.sign_user import SignUser
from zhenxun.models.statistics import Statistics
from zhenxun.models.user_console import UserConsole
from zhenxun.services import avatar_service
from zhenxun.services.db_context import with_db_timeout
from zhenxun.services.message_load import is_db_unhealthy
from zhenxun.utils.platform import PlatformUtils
RACE = [
@@ -81,6 +83,21 @@ lik2level = {
10: 1,
0: 0,
}
_INFO_DB_TIMEOUT = 3.0
async def _read_db(factory, operation: str, default):
if is_db_unhealthy():
return default
try:
return await with_db_timeout(
factory(),
timeout=_INFO_DB_TIMEOUT,
operation=operation,
source="my_info",
)
except Exception:
return default
def get_level(impression: float) -> int:
@@ -103,13 +120,17 @@ async def get_chat_history(
"""
now = datetime.now()
filter_date = now - timedelta(days=7)
date_list = (
await ChatHistory.filter(
user_id=user_id, group_id=group_id, create_time__gte=filter_date
date_list = await _read_db(
lambda: ChatHistory.filter(
user_id=user_id,
group_id=group_id,
create_time__gte=filter_date,
)
.annotate(date=RawSQL("DATE(create_time)"), count=Count("id"))
.group_by("date")
.values("date", "count")
.values("date", "count"),
"MyInfo.chat_history_chart",
[],
)
chart_date: list[str] = []
count_list: list[int] = []
@@ -143,20 +164,40 @@ async def get_user_info(
avatar_path = await avatar_service.get_avatar_path(platform, user_id)
avatar_url = avatar_path.as_uri() if avatar_path else ""
user = await UserConsole.get_user(user_id, platform)
permission_level = await LevelUser.get_user_level(user_id, group_id)
user = await _read_db(
lambda: UserConsole.get_user(user_id, platform),
"MyInfo.user_console",
None,
)
permission_level = await _read_db(
lambda: LevelUser.get_user_level(user_id, group_id),
"MyInfo.level_user",
0,
)
sign_level = 0
if sign_user := await SignUser.get_or_none(user_id=user_id):
if sign_user := await _read_db(
lambda: SignUser.get_or_none(user_id=user_id),
"MyInfo.sign_user",
None,
):
sign_level = get_level(float(sign_user.impression))
chat_count = await ChatHistory.filter(user_id=user_id, group_id=group_id).count()
stat_count = await Statistics.filter(user_id=user_id, group_id=group_id).count()
chat_count = await _read_db(
lambda: ChatHistory.filter(user_id=user_id, group_id=group_id).count(),
"MyInfo.chat_count",
0,
)
stat_count = await _read_db(
lambda: Statistics.filter(user_id=user_id, group_id=group_id).count(),
"MyInfo.stat_count",
0,
)
selected_indices = [""] * 9
selected_indices[sign_level] = "select"
uid = f"{user.uid}".rjust(8, "0")
uid = f"{getattr(user, 'uid', 0)}".rjust(8, "0")
uid_formatted = f"{uid[:4]} {uid[4:]}"
now = datetime.now()
@@ -182,8 +223,8 @@ async def get_user_info(
),
},
"stats": {
"gold": user.gold,
"prop_count": len(user.props),
"gold": getattr(user, "gold", 0),
"prop_count": len(getattr(user, "props", {}) or {}),
"call_count": stat_count,
"chat_count": chat_count,
},
+2 -15
View File
@@ -4,10 +4,7 @@
负责注册各种缓存类型,实现按需缓存机制
"""
from zhenxun.models.ban_console import BanConsole
from zhenxun.models.bot_console import BotConsole
from zhenxun.models.group_console import GroupConsole
from zhenxun.models.group_plugin_setting import GroupPluginSetting
from zhenxun.models.level_user import LevelUser
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.user_console import UserConsole
@@ -21,21 +18,11 @@ from zhenxun.utils.enum import CacheType
def register_cache_types():
"""注册所有缓存类型"""
CacheRegistry.register(CacheType.PLUGINS, PluginInfo)
CacheRegistry.register(CacheType.GROUPS, GroupConsole)
CacheRegistry.register(CacheType.BOT, BotConsole)
CacheRegistry.register(CacheType.USERS, UserConsole)
CacheRegistry.register(
CacheType.GROUP_PLUGIN_SETTINGS,
GroupPluginSetting,
key_format="{group_id}_{plugin_name}_{key}",
)
CacheRegistry.register(
CacheType.LEVEL, LevelUser, key_format="{user_id}_{group_id}"
)
CacheRegistry.register(CacheType.BAN, BanConsole, key_format="{user_id}_{group_id}")
if cache_config.cache_mode == CacheMode.NONE:
logger.info("缓存功能已禁用,将直接从数据库获取数据")
else:
logger.info(f"已注册所有缓存类型,缓存模式: {cache_config.cache_mode}")
logger.info("使用增量缓存模式,数据将按需加载到缓存中")
if cache_config.cache_mode == CacheMode.REDIS and cache_config.redis_host:
logger.info(f"已注册 Redis 模型缓存类型,缓存模式: {cache_config.cache_mode}")
@@ -314,7 +314,12 @@ class StoreManager:
is_external,
source,
)
return f"插件 {plugin_info.name} 安装成功! 重启后生效"
return (
f"插件 {plugin_info.name} 安装完成\n"
"- 已下载插件文件\n"
"- 已处理依赖文件\n"
"- 重启后生效"
)
@classmethod
async def install_plugin_with_repo(
@@ -7,14 +7,19 @@ from nonebot.adapters.onebot.v11 import PokeNotifyEvent
from nonebot.matcher import Matcher
from nonebot.message import run_postprocessor
from nonebot.plugin import PluginMetadata
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.utils import PluginExtraData
from zhenxun.models.statistics import Statistics
from zhenxun.services.cache.runtime_cache import PluginInfoMemoryCache
from zhenxun.services.db_context import with_db_timeout
from zhenxun.services.log import logger
from zhenxun.services.message_load import should_pause_tasks
from zhenxun.services.low_priority_writer import (
LowPriorityWriterConfig,
append_low_priority_record,
flush_low_priority_writer,
register_low_priority_writer,
)
from zhenxun.utils.enum import PluginType
from zhenxun.utils.utils import get_entity_ids
@@ -29,37 +34,44 @@ __plugin_meta__ = PluginMetadata(
STATS_BUFFER_FLUSH_SIZE = 5000
STATS_BUFFER_MAX_RETAIN = 10000
TEMP_LIST: list[Statistics] = []
_STATS_FLUSH_LOCK = asyncio.Lock()
_WRITER_NAME = "statistics"
driver = get_driver()
async def _write_statistics_batch(batch: list[Statistics], reason: str) -> None:
await with_db_timeout(
Statistics.bulk_create(batch),
timeout=5.0,
operation=f"Statistics.bulk_create[{len(batch)}]",
source=f"statistics:{reason}",
)
register_low_priority_writer(
LowPriorityWriterConfig(
name=_WRITER_NAME,
write_batch=_write_statistics_batch,
batch_size=STATS_BUFFER_FLUSH_SIZE,
trigger_size=STATS_BUFFER_FLUSH_SIZE,
max_retain=STATS_BUFFER_MAX_RETAIN,
flush_interval_seconds=30 * 60,
max_items_per_cycle=STATS_BUFFER_FLUSH_SIZE,
backoff_base_seconds=30.0,
backoff_max_seconds=600.0,
log_command="定时任务",
)
)
async def _flush_statistics_buffer(reason: str) -> int:
"""Compatibility entry used by memory governor and shutdown hooks."""
async with _STATS_FLUSH_LOCK:
call_list = TEMP_LIST.copy()
TEMP_LIST.clear()
if not call_list:
return 0
try:
await Statistics.bulk_create(call_list)
except Exception as e:
logger.error(f"{reason}批量添加调用记录失败", "定时任务", e=e)
retain_count = max(STATS_BUFFER_MAX_RETAIN - len(TEMP_LIST), 0)
if retain_count:
TEMP_LIST[:0] = call_list[-retain_count:]
return 0
logger.debug(f"{reason}批量添加调用记录 {len(call_list)} 条", "定时任务")
return len(call_list)
return await flush_low_priority_writer(_WRITER_NAME, reason, force=True)
async def _append_statistics(record: Statistics) -> None:
# 在锁内追加(B8),与 flush 的 copy+clear 串行,消除逻辑窗口;
# flush 自身再次获取同一把锁,故在锁外触发避免重入。
async with _STATS_FLUSH_LOCK:
TEMP_LIST.append(record)
should_flush = len(TEMP_LIST) >= STATS_BUFFER_FLUSH_SIZE
if should_flush:
await _flush_statistics_buffer("缓冲区触发")
await append_low_priority_record(_WRITER_NAME, record)
@run_postprocessor
@@ -95,16 +107,6 @@ async def _(
)
@scheduler.scheduled_job("interval", minutes=30, max_instances=1, coalesce=True)
async def _():
try:
if should_pause_tasks():
return
await _flush_statistics_buffer("定时")
except Exception as e:
logger.error("定时批量添加调用记录", "定时任务", e=e)
@driver.on_shutdown
async def _flush_statistics_on_shutdown():
await _flush_statistics_buffer("关闭")
@@ -7,7 +7,7 @@ from nonebot.config import Config
from zhenxun.services.log import logger
from ....base_model import BaseResultModel, QueryModel, Result
from ....utils import authentication
from ....utils import DB_BUSY_MESSAGE, authentication
from .data_source import ApiDataSource
from .model import AllChatAndCallCount, BotInfo, ChatCallMonthCount, QueryChatCallCount
@@ -28,6 +28,8 @@ driver = nonebot.get_driver()
async def _() -> Result[list[BotInfo]]:
try:
return Result.ok(await ApiDataSource.get_bot_list(), "拿到信息啦!")
except TimeoutError:
return Result.fail(DB_BUSY_MESSAGE)
except Exception as e:
logger.error(f"{router.prefix}/get_bot_list 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@@ -45,6 +47,8 @@ async def _(bot_id: str | None = None) -> Result[QueryChatCallCount]:
return Result.ok(
await ApiDataSource.get_chat_and_call_count(bot_id), "拿到信息啦!"
)
except TimeoutError:
return Result.fail(DB_BUSY_MESSAGE)
except Exception as e:
logger.error(f"{router.prefix}/get_chat_and_call_count 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@@ -62,6 +66,8 @@ async def _(bot_id: str | None = None) -> Result[AllChatAndCallCount]:
return Result.ok(
await ApiDataSource.get_all_chat_and_call_count(bot_id), "拿到信息啦!"
)
except TimeoutError:
return Result.fail(DB_BUSY_MESSAGE)
except Exception as e:
logger.error(
f"{router.prefix}/get_all_chat_and_call_count 调用错误", "WebUi", e=e
@@ -81,6 +87,8 @@ async def _(bot_id: str | None = None) -> Result[ChatCallMonthCount]:
return Result.ok(
await ApiDataSource.get_chat_and_call_month(bot_id), "拿到信息啦!"
)
except TimeoutError:
return Result.fail(DB_BUSY_MESSAGE)
except Exception as e:
logger.error(f"{router.prefix}/get_chat_and_call_month 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@@ -17,6 +17,7 @@ from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.platform import PlatformUtils
from ....base_model import BaseResultModel, QueryModel
from ....utils import webui_db_call
from ..main.data_source import bot_live
from .model import (
AllChatAndCallCount,
@@ -77,14 +78,20 @@ class ApiDataSource:
logger.warning("获取bot好友/群组信息失败...", "WebUi", e=e)
bot_info.group_count = 0
bot_info.friend_count = 0
bot_info.day_call = await Statistics.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute),
bot_id=bot.self_id,
).count()
bot_info.received_messages = await ChatHistory.filter(
bot_id=bot_info.self_id,
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute),
).count()
bot_info.day_call = await webui_db_call(
Statistics.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute),
bot_id=bot.self_id,
).count(),
"Dashboard.bot_day_call",
)
bot_info.received_messages = await webui_db_call(
ChatHistory.filter(
bot_id=bot_info.self_id,
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute),
).count(),
"Dashboard.bot_received_messages",
)
bot_info.connect_time = bot_live.get(bot.self_id) or 0
if bot_info.connect_time:
connect_date = datetime.fromtimestamp(CONNECT_TIME)
@@ -117,17 +124,29 @@ class ApiDataSource:
query = ChatHistory
if bot_id:
query = query.filter(bot_id=bot_id)
chat_all_count = await query.annotate().count()
chat_day_count = await query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count()
chat_all_count = await webui_db_call(
query.annotate().count(),
"Dashboard.chat_all_count",
)
chat_day_count = await webui_db_call(
query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count(),
"Dashboard.chat_day_count",
)
query = Statistics
if bot_id:
query = query.filter(bot_id=bot_id)
call_all_count = await query.annotate().count()
call_day_count = await query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count()
call_all_count = await webui_db_call(
query.annotate().count(),
"Dashboard.call_all_count",
)
call_day_count = await webui_db_call(
query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count(),
"Dashboard.call_day_count",
)
return QueryChatCallCount(
chat_num=chat_all_count,
chat_day=chat_day_count,
@@ -151,31 +170,51 @@ class ApiDataSource:
query = ChatHistory
if bot_id:
query = query.filter(bot_id=bot_id)
chat_week_count = await query.filter(
create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute)
).count()
chat_month_count = await query.filter(
create_time__gte=now
- timedelta(days=30, hours=now.hour, minutes=now.minute)
).count()
chat_year_count = await query.filter(
create_time__gte=now
- timedelta(days=365, hours=now.hour, minutes=now.minute)
).count()
chat_week_count = await webui_db_call(
query.filter(
create_time__gte=now
- timedelta(days=7, hours=now.hour, minutes=now.minute)
).count(),
"Dashboard.chat_week_count",
)
chat_month_count = await webui_db_call(
query.filter(
create_time__gte=now
- timedelta(days=30, hours=now.hour, minutes=now.minute)
).count(),
"Dashboard.chat_month_count",
)
chat_year_count = await webui_db_call(
query.filter(
create_time__gte=now
- timedelta(days=365, hours=now.hour, minutes=now.minute)
).count(),
"Dashboard.chat_year_count",
)
query = Statistics
if bot_id:
query = query.filter(bot_id=bot_id)
call_week_count = await query.filter(
create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute)
).count()
call_month_count = await query.filter(
create_time__gte=now
- timedelta(days=30, hours=now.hour, minutes=now.minute)
).count()
call_year_count = await query.filter(
create_time__gte=now
- timedelta(days=365, hours=now.hour, minutes=now.minute)
).count()
call_week_count = await webui_db_call(
query.filter(
create_time__gte=now
- timedelta(days=7, hours=now.hour, minutes=now.minute)
).count(),
"Dashboard.call_week_count",
)
call_month_count = await webui_db_call(
query.filter(
create_time__gte=now
- timedelta(days=30, hours=now.hour, minutes=now.minute)
).count(),
"Dashboard.call_month_count",
)
call_year_count = await webui_db_call(
query.filter(
create_time__gte=now
- timedelta(days=365, hours=now.hour, minutes=now.minute)
).count(),
"Dashboard.call_year_count",
)
return AllChatAndCallCount(
chat_week=chat_week_count,
chat_month=chat_month_count,
@@ -202,17 +241,19 @@ class ApiDataSource:
if bot_id:
chat_query = chat_query.filter(bot_id=bot_id)
call_query = call_query.filter(bot_id=bot_id)
chat_date_list = (
await chat_query.filter(create_time__gte=filter_date)
chat_date_list = await webui_db_call(
chat_query.filter(create_time__gte=filter_date)
.annotate(date=RawSQL("DATE(create_time)"), count=Count("id"))
.group_by("date")
.values("date", "count")
.values("date", "count"),
"Dashboard.chat_month_series",
)
call_date_list = (
await call_query.filter(create_time__gte=filter_date)
call_date_list = await webui_db_call(
call_query.filter(create_time__gte=filter_date)
.annotate(date=RawSQL("DATE(create_time)"), count=Count("id"))
.group_by("date")
.values("date", "count")
.values("date", "count"),
"Dashboard.call_month_series",
)
date_list = []
chat_count_list = []
@@ -16,7 +16,7 @@ from zhenxun.utils.platform import PlatformUtils
from ....base_model import Result
from ....config import QueryDateType
from ....utils import authentication, get_system_status
from ....utils import DB_BUSY_MESSAGE, authentication, get_system_status
from .data_source import ApiDataSource
from .model import (
ActiveGroup,
@@ -82,6 +82,8 @@ async def _(bot_id: str | None = None) -> Result[list[BaseInfo]]:
if not result:
Result.warning_("无Bot连接...")
return Result.ok(result, "拿到信息啦!")
except TimeoutError:
return Result.fail(DB_BUSY_MESSAGE)
except Exception as e:
logger.error(f"{router.prefix}/get_base_info 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@@ -97,6 +99,8 @@ async def _(bot_id: str | None = None) -> Result[list[BaseInfo]]:
async def _(bot_id: str | None = None) -> Result[QueryCount]:
try:
return Result.ok(await ApiDataSource.get_all_chat_count(bot_id), "拿到信息啦!")
except TimeoutError:
return Result.fail(DB_BUSY_MESSAGE)
except Exception as e:
logger.error(f"{router.prefix}/get_all_chat_count 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@@ -112,6 +116,8 @@ async def _(bot_id: str | None = None) -> Result[QueryCount]:
async def _(bot_id: str | None = None) -> Result[QueryCount]:
try:
return Result.ok(await ApiDataSource.get_all_call_count(bot_id), "拿到信息啦!")
except TimeoutError:
return Result.fail(DB_BUSY_MESSAGE)
except Exception as e:
logger.error(f"{router.prefix}/get_all_call_count 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@@ -188,6 +194,8 @@ async def _(
return Result.ok(
await ApiDataSource.get_active_group(date_type, bot_id), "拿到信息啦!"
)
except TimeoutError:
return Result.fail(DB_BUSY_MESSAGE)
except Exception as e:
logger.error(f"{router.prefix}/get_active_group 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@@ -207,6 +215,8 @@ async def _(
return Result.ok(
await ApiDataSource.get_hot_plugin(date_type, bot_id), "拿到信息啦!"
)
except TimeoutError:
return Result.fail(DB_BUSY_MESSAGE)
except Exception as e:
logger.error(f"{router.prefix}/get_hot_plugin 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@@ -294,9 +304,14 @@ async def system_logs_realtime(websocket: WebSocket, sleep: int = 5):
await asyncio.wait_for(websocket.send_text(system_status.json()), timeout=5)
try:
await asyncio.wait_for(disconnect_event.wait(), timeout=max(sleep, 1))
except TimeoutError:
except asyncio.TimeoutError:
pass
except (WebSocketDisconnect, ConnectionClosedError, ConnectionClosedOK):
except (
asyncio.CancelledError,
WebSocketDisconnect,
ConnectionClosedError,
ConnectionClosedOK,
):
pass
finally:
_SYSTEM_STATUS_CONNECTIONS.discard(websocket)
@@ -20,6 +20,7 @@ from zhenxun.utils.enum import PluginType
from zhenxun.utils.platform import PlatformUtils
from ....config import AVA_URL, GROUP_AVA_URL, QueryDateType
from ....utils import webui_db_call
from .model import (
ActiveGroup,
BaseInfo,
@@ -105,10 +106,13 @@ class ApiDataSource:
"""
now = datetime.now()
# 今日累计接收消息
select_bot.received_messages = await ChatHistory.filter(
bot_id=select_bot.self_id,
create_time__gte=now - timedelta(hours=now.hour),
).count()
select_bot.received_messages = await webui_db_call(
ChatHistory.filter(
bot_id=select_bot.self_id,
create_time__gte=now - timedelta(hours=now.hour),
).count(),
"Main.received_messages",
)
# 群聊数量
try:
select_bot.group_count = len(
@@ -129,13 +133,15 @@ class ApiDataSource:
connect_date = datetime.fromtimestamp(select_bot.connect_time)
select_bot.connect_date = connect_date.strftime("%Y-%m-%d %H:%M:%S")
select_bot.version = cls.__get_bot_version()
day_call = await Statistics.filter(
create_time__gte=now - timedelta(hours=now.hour)
).count()
day_call = await webui_db_call(
Statistics.filter(create_time__gte=now - timedelta(hours=now.hour)).count(),
"Main.day_call",
)
select_bot.day_call = day_call
select_bot.connect_count = await BotConnectLog.filter(
bot_id=select_bot.self_id
).count()
select_bot.connect_count = await webui_db_call(
BotConnectLog.filter(bot_id=select_bot.self_id).count(),
"Main.connect_count",
)
@classmethod
async def get_base_info(cls, bot_id: str | None) -> list[BaseInfo] | None:
@@ -177,21 +183,37 @@ class ApiDataSource:
query = ChatHistory
if bot_id:
query = query.filter(bot_id=bot_id)
all_count = await query.annotate().count()
day_count = await query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count()
week_count = await query.filter(
create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute)
).count()
month_count = await query.filter(
create_time__gte=now
- timedelta(days=30, hours=now.hour, minutes=now.minute)
).count()
year_count = await query.filter(
create_time__gte=now
- timedelta(days=365, hours=now.hour, minutes=now.minute)
).count()
all_count = await webui_db_call(
query.annotate().count(),
"Main.chat_all_count",
)
day_count = await webui_db_call(
query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count(),
"Main.chat_day_count",
)
week_count = await webui_db_call(
query.filter(
create_time__gte=now
- timedelta(days=7, hours=now.hour, minutes=now.minute)
).count(),
"Main.chat_week_count",
)
month_count = await webui_db_call(
query.filter(
create_time__gte=now
- timedelta(days=30, hours=now.hour, minutes=now.minute)
).count(),
"Main.chat_month_count",
)
year_count = await webui_db_call(
query.filter(
create_time__gte=now
- timedelta(days=365, hours=now.hour, minutes=now.minute)
).count(),
"Main.chat_year_count",
)
return QueryCount(
num=all_count,
day=day_count,
@@ -214,21 +236,37 @@ class ApiDataSource:
query = Statistics
if bot_id:
query = query.filter(bot_id=bot_id)
all_count = await query.annotate().count()
day_count = await query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count()
week_count = await query.filter(
create_time__gte=now - timedelta(days=7, hours=now.hour, minutes=now.minute)
).count()
month_count = await query.filter(
create_time__gte=now
- timedelta(days=30, hours=now.hour, minutes=now.minute)
).count()
year_count = await query.filter(
create_time__gte=now
- timedelta(days=365, hours=now.hour, minutes=now.minute)
).count()
all_count = await webui_db_call(
query.annotate().count(),
"Main.call_all_count",
)
day_count = await webui_db_call(
query.filter(
create_time__gte=now - timedelta(hours=now.hour, minutes=now.minute)
).count(),
"Main.call_day_count",
)
week_count = await webui_db_call(
query.filter(
create_time__gte=now
- timedelta(days=7, hours=now.hour, minutes=now.minute)
).count(),
"Main.call_week_count",
)
month_count = await webui_db_call(
query.filter(
create_time__gte=now
- timedelta(days=30, hours=now.hour, minutes=now.minute)
).count(),
"Main.call_month_count",
)
year_count = await webui_db_call(
query.filter(
create_time__gte=now
- timedelta(days=365, hours=now.hour, minutes=now.minute)
).count(),
"Main.call_year_count",
)
return QueryCount(
num=all_count,
day=day_count,
@@ -296,19 +334,21 @@ class ApiDataSource:
list[ActiveGroup]: 活跃群组列表
"""
query = cls.__get_query(ChatHistory, date_type, bot_id)
data_list = (
await query.annotate(count=Count("id"))
data_list = await webui_db_call(
query.annotate(count=Count("id"))
.filter(group_id__not_isnull=True)
.group_by("group_id")
.order_by("-count")
.limit(5)
.values_list("group_id", "count")
.values_list("group_id", "count"),
"Main.active_group",
)
id2name = {}
if data_list:
if info_list := await GroupConsole.filter(
group_id__in=[x[0] for x in data_list]
).all():
if info_list := await webui_db_call(
GroupConsole.filter(group_id__in=[x[0] for x in data_list]).all(),
"Main.active_group_names",
):
for group_info in info_list:
id2name[group_info.group_id] = group_info.group_name
active_group_list = [
@@ -341,19 +381,23 @@ class ApiDataSource:
list[HotPlugin]: 热门插件列表
"""
query = cls.__get_query(Statistics, date_type, bot_id)
data_list = (
await query.annotate(count=Count("id"))
data_list = await webui_db_call(
query.annotate(count=Count("id"))
.group_by("plugin_name")
.order_by("-count")
.limit(5)
.values_list("plugin_name", "count")
.values_list("plugin_name", "count"),
"Main.hot_plugin",
)
hot_plugin_list = []
module_list = [x[0] for x in data_list]
plugins = await PluginInfo.get_plugins(
load_status=None,
filter_parent=False,
module__in=module_list,
plugins = await webui_db_call(
PluginInfo.get_plugins(
load_status=None,
filter_parent=False,
module__in=module_list,
),
"Main.hot_plugin_names",
)
module2name = {p.module: p.name for p in plugins}
for data in data_list:
@@ -12,7 +12,7 @@ from zhenxun.utils.platform import PlatformUtils
from ....base_model import Result
from ....config import AVA_URL, GROUP_AVA_URL
from ....utils import authentication
from ....utils import DB_BUSY_MESSAGE, authentication
from .data_source import ApiDataSource
from .model import (
ClearRequest,
@@ -294,6 +294,8 @@ async def _(bot_id: str, user_id: str) -> Result[UserDetail]:
if result
else Result.warning_("未找到该好友...")
)
except TimeoutError:
return Result.fail(DB_BUSY_MESSAGE)
except (ValueError, KeyError):
return Result.warning_("指定Bot未连接...")
except Exception as e:
@@ -311,6 +313,8 @@ async def _(bot_id: str, user_id: str) -> Result[UserDetail]:
async def _(group_id: str) -> Result[GroupDetail]:
try:
return Result.ok(await ApiDataSource.get_group_detail(group_id), "拿到信息啦!")
except TimeoutError:
return Result.fail(DB_BUSY_MESSAGE)
except Exception as e:
logger.error(f"{router.prefix}/get_group_detail 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@@ -13,6 +13,7 @@ from zhenxun.utils.enum import RequestType
from zhenxun.utils.platform import PlatformUtils
from ....config import AVA_URL, GROUP_AVA_URL
from ....utils import webui_db_call
from .model import (
FriendRequestResult,
GroupDetail,
@@ -110,20 +111,24 @@ class ApiDataSource:
fd = [x for x in friend_list if x.user_id == user_id]
if not fd:
return None
like_plugin_list = (
await Statistics.filter(user_id=user_id)
like_plugin_list = await webui_db_call(
Statistics.filter(user_id=user_id)
.annotate(count=Count("id"))
.group_by("plugin_name")
.order_by("-count")
.limit(5)
.values_list("plugin_name", "count")
.values_list("plugin_name", "count"),
"Manage.friend_like_plugin",
)
like_plugin = {}
module_list = [x[0] for x in like_plugin_list]
plugins = await PluginInfo.get_plugins(
load_status=None,
filter_parent=False,
module__in=module_list,
plugins = await webui_db_call(
PluginInfo.get_plugins(
load_status=None,
filter_parent=False,
module__in=module_list,
),
"Manage.friend_like_plugin_names",
)
module2name = {p.module: p.name for p in plugins}
for data in like_plugin_list:
@@ -136,8 +141,14 @@ class ApiDataSource:
nickname=user.user_name,
remark="",
is_ban=await BanConsole.is_ban(user_id),
chat_count=await ChatHistory.filter(user_id=user_id).count(),
call_count=await Statistics.filter(user_id=user_id).count(),
chat_count=await webui_db_call(
ChatHistory.filter(user_id=user_id).count(),
"Manage.friend_chat_count",
),
call_count=await webui_db_call(
Statistics.filter(user_id=user_id).count(),
"Manage.friend_call_count",
),
like_plugin=like_plugin,
)
@@ -151,16 +162,20 @@ class ApiDataSource:
返回:
dict[str, int]: 插件与调用次数
"""
like_plugin_list = (
await Statistics.filter(group_id=group_id)
like_plugin_list = await webui_db_call(
Statistics.filter(group_id=group_id)
.annotate(count=Count("id"))
.group_by("plugin_name")
.order_by("-count")
.limit(5)
.values_list("plugin_name", "count")
.values_list("plugin_name", "count"),
"Manage.group_like_plugin",
)
like_plugin = {}
plugins = await PluginInfo.get_plugins()
plugins = await webui_db_call(
PluginInfo.get_plugins(),
"Manage.group_like_plugin_names",
)
module2name = {p.module: p.name for p in plugins}
for data in like_plugin_list:
name = module2name.get(data[0]) or data[0]
@@ -268,8 +283,14 @@ class ApiDataSource:
name=group.group_name,
member_count=group.member_count,
max_member_count=group.max_member_count,
chat_count=await ChatHistory.filter(group_id=group_id).count(),
call_count=await Statistics.filter(group_id=group_id).count(),
chat_count=await webui_db_call(
ChatHistory.filter(group_id=group_id).count(),
"Manage.group_chat_count",
),
call_count=await webui_db_call(
Statistics.filter(group_id=group_id).count(),
"Manage.group_call_count",
),
like_plugin=like_plugin,
level=group.level,
status=group.status,
@@ -49,6 +49,7 @@ async def _(param: PluginIr) -> Result:
from zhenxun.builtin_plugins.plugin_store import StoreManager
result = await StoreManager.add_plugin(str(param.id)) # type: ignore
logger.info(result.replace("\n", ";"), "插件商店")
return Result.ok(info=result)
except Exception as e:
return Result.fail(f"安装插件失败: {type(e)}: {e}")
+18
View File
@@ -12,11 +12,15 @@ import ujson as json
from zhenxun.configs.config import Config
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.db_context import with_db_timeout
from zhenxun.services.message_load import is_db_unhealthy
from .base_model import SystemFolderSize, SystemStatus, User
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
DB_BUSY_MESSAGE = "数据库繁忙,请稍后再试"
WEBUI_DB_TIMEOUT = 3.0
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/login")
@@ -28,6 +32,20 @@ if token_file.exists():
token_data = json.load(open(token_file, encoding="utf8"))
async def webui_db_call(coro, operation: str):
if is_db_unhealthy():
close = getattr(coro, "close", None)
if callable(close):
close()
raise TimeoutError(DB_BUSY_MESSAGE)
return await with_db_timeout(
coro,
timeout=WEBUI_DB_TIMEOUT,
operation=operation,
source="web_ui",
)
def validate_path(path_str: str | None) -> tuple[Path | None, str | None]:
"""验证路径是否安全