mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-21 12:10:13 +08:00
bugfix:修复sqlite部分场景下锁竞争问题;新增插件恶意触发配置文件 (#2144)
* bugfix:修复sqlite部分场景下锁竞争问题;新增插件恶意触发配置文件 * 文件没同步完
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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]:
|
||||
"""验证路径是否安全
|
||||
|
||||
|
||||
Reference in New Issue
Block a user