From 0997321b352b1caf7375018f64a5ada5c7dc6b6a Mon Sep 17 00:00:00 2001 From: RuochenPan Date: Tue, 21 Jul 2026 17:18:25 +0800 Subject: [PATCH 1/7] feat(logging): add isolated plugin loggers --- astrbot/api/__init__.py | 65 ++++++++++++- astrbot/core/log.py | 170 ++++++++++++++++++++++++++++++---- astrbot/core/star/base.py | 26 ++++++ docs/zh/dev/star/plugin.md | 1 + tests/unit/test_api_logger.py | 65 +++++++++++++ tests/unit/test_star_base.py | 28 ++++++ 6 files changed, 337 insertions(+), 18 deletions(-) create mode 100644 tests/unit/test_api_logger.py diff --git a/astrbot/api/__init__.py b/astrbot/api/__init__.py index 5d15dedc20..5c2d4598f8 100644 --- a/astrbot/api/__init__.py +++ b/astrbot/api/__init__.py @@ -1,4 +1,6 @@ -from astrbot import logger +import logging +import sys + from astrbot.core import html_renderer, sp from astrbot.core.agent.tool import FunctionTool, ToolSet from astrbot.core.agent.tool_executor import BaseFunctionToolExecutor @@ -6,6 +8,67 @@ from astrbot.core.star.register import register_agent as agent from astrbot.core.star.register import register_llm_tool as llm_tool +_fallback_logger = logging.getLogger("astrbot") +_logger_cache: dict[ + str, + tuple[str | None, str | None, logging.Logger], +] = {} + +# Caller modules under these roots may belong to plugins that are not +# registered yet, so resolution failures for them are never cached. +_PLUGIN_MODULE_ROOTS = ("data.plugins.", "astrbot.builtin_stars.") + + +def _resolve_caller_logger(module_name: str) -> logging.Logger: + """Resolve the dedicated plugin logger for a caller module. + + Args: + module_name: The ``__name__`` of the module that called the logger. + + Returns: + The plugin's dedicated logger, or the global ``astrbot`` logger when + the caller does not belong to a registered plugin. + """ + # Imported lazily to avoid a circular import with astrbot.core.star. + from astrbot.core.star.star import star_map + + cached = _logger_cache.get(module_name) + if cached is not None: + module_path, plugin_name, cached_logger = cached + if module_path is None: + return cached_logger + metadata = star_map.get(module_path) + if metadata is not None and metadata.name == plugin_name: + return cached_logger + _logger_cache.pop(module_name, None) + + for module_path, metadata in star_map.items(): + if not module_path or not metadata.name: + continue + package = module_path.rpartition(".")[0] + if module_name == module_path or module_name.startswith(package + "."): + resolved = logging.getLogger(f"astrbot.plugin.{metadata.name}") + _logger_cache[module_name] = (module_path, metadata.name, resolved) + return resolved + + if not module_name.startswith(_PLUGIN_MODULE_ROOTS): + _logger_cache[module_name] = (None, None, _fallback_logger) + return _fallback_logger + + +class _PluginContextLogger: + """Proxy routing ``astrbot.api.logger`` calls to the caller plugin's logger.""" + + def __getattr__(self, item: str): + module_name = sys._getframe(1).f_globals.get("__name__", "") + return getattr(_resolve_caller_logger(module_name), item) + + +logger = _PluginContextLogger() +"""Plugin-facing logger. Calls are routed to the calling plugin's dedicated +logger (``astrbot.plugin.``) so each plugin's log level can be +tuned independently; non-plugin callers fall back to the global logger.""" + __all__ = [ "AstrBotConfig", "BaseFunctionToolExecutor", diff --git a/astrbot/core/log.py b/astrbot/core/log.py index 3dd0719b11..21dcedf58a 100644 --- a/astrbot/core/log.py +++ b/astrbot/core/log.py @@ -1,6 +1,7 @@ """日志系统,统一将标准 logging 输出转发到 loguru。""" import asyncio +import json import logging import os import sys @@ -12,10 +13,19 @@ from loguru import logger as _raw_loguru_logger from astrbot.core.config.default import VERSION -from astrbot.core.utils.astrbot_path import get_astrbot_data_path +from astrbot.core.utils.astrbot_path import ( + get_astrbot_config_path, + get_astrbot_data_path, +) CACHED_SIZE = 500 +PLUGIN_LOGGER_PREFIX = "astrbot.plugin." +"""Prefix of per-plugin logger names; full name is ``astrbot.plugin.``.""" + +PLUGIN_LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") +"""Allowed per-plugin log levels.""" + if TYPE_CHECKING: from loguru import Record @@ -24,7 +34,13 @@ class _RecordEnricherFilter(logging.Filter): """为 logging.LogRecord 注入 AstrBot 日志字段。""" def filter(self, record: logging.LogRecord) -> bool: - record.plugin_tag = "[Plug]" if _is_plugin_path(record.pathname) else "[Core]" + if record.name.startswith(PLUGIN_LOGGER_PREFIX): + # Records from a per-plugin logger are tagged with the plugin name. + record.plugin_tag = f"[{record.name[len(PLUGIN_LOGGER_PREFIX) :]}]" + else: + record.plugin_tag = ( + "[Plug]" if _is_plugin_path(record.pathname) else "[Core]" + ) record.short_levelname = _get_short_level_name(record.levelname) record.astrbot_version_tag = ( f" [v{VERSION}]" if record.levelno >= logging.WARNING else "" @@ -173,6 +189,9 @@ class LogManager: _console_sink_id: int | None = None _file_sink_id: int | None = None _trace_sink_id: int | None = None + _plugin_logger_names: set[str] = set() + _plugin_level_overrides: dict[str, str] | None = None + _log_broker: "LogBroker | None" = None _NOISY_LOGGER_LEVELS: dict[str, int] = { "aiosqlite": logging.WARNING, "filelock": logging.WARNING, @@ -262,25 +281,126 @@ def GetLogger(cls, log_name: str = "default") -> logging.Logger: logger.propagate = False return logger + @classmethod + def _plugin_log_levels_path(cls) -> str: + return os.path.join(get_astrbot_config_path(), "plugin_log_levels.json") + + @classmethod + def _load_plugin_level_overrides(cls) -> dict[str, str]: + """Lazily load persisted per-plugin log level overrides from disk.""" + if cls._plugin_level_overrides is None: + cls._plugin_level_overrides = {} + try: + with open(cls._plugin_log_levels_path(), encoding="utf-8") as f: + data = json.load(f) + if isinstance(data, dict): + cls._plugin_level_overrides = { + str(name): str(level).upper() + for name, level in data.items() + if str(level).upper() in PLUGIN_LOG_LEVELS + } + except (OSError, ValueError): + pass + return cls._plugin_level_overrides + + @classmethod + def get_plugin_log_level(cls, plugin_name: str) -> str | None: + """Get the log level override of a plugin. + + Args: + plugin_name: The plugin name. + + Returns: + The configured level name, or None if the plugin follows the global level. + """ + return cls._load_plugin_level_overrides().get(plugin_name) + + @classmethod + def set_plugin_log_level(cls, plugin_name: str, level: str | None) -> None: + """Persist and apply a per-plugin log level override. + + Args: + plugin_name: The plugin name. + level: The level name to apply, or None to follow the global level. + + Raises: + ValueError: If the level name is not valid. + """ + overrides = cls._load_plugin_level_overrides() + if level is None: + overrides.pop(plugin_name, None) + else: + level = level.upper() + if level not in PLUGIN_LOG_LEVELS: + raise ValueError(f"Invalid log level: {level}") + overrides[plugin_name] = level + + os.makedirs(os.path.dirname(cls._plugin_log_levels_path()), exist_ok=True) + with open(cls._plugin_log_levels_path(), "w", encoding="utf-8") as f: + json.dump(overrides, f, indent=2) + + if plugin_name in cls._plugin_logger_names: + logging.getLogger(f"{PLUGIN_LOGGER_PREFIX}{plugin_name}").setLevel( + cls._effective_plugin_log_level(plugin_name) + ) + + @classmethod + def _effective_plugin_log_level(cls, plugin_name: str) -> int: + override = cls.get_plugin_log_level(plugin_name) + if override: + return logging.getLevelName(override) + base_level = logging.getLogger("astrbot").level + return base_level if base_level > 0 else logging.INFO + + @classmethod + def get_plugin_logger(cls, plugin_name: str) -> logging.Logger: + """Get or create the dedicated logger for a plugin. + + The logger is isolated from the global ``astrbot`` logger so its level + can be tuned independently. Its level defaults to the persisted + per-plugin override, falling back to the current global level. + + Args: + plugin_name: The plugin name. + + Returns: + The plugin's dedicated logger. + """ + plugin_logger = cls.GetLogger(f"{PLUGIN_LOGGER_PREFIX}{plugin_name}") + if plugin_name not in cls._plugin_logger_names: + cls._plugin_logger_names.add(plugin_name) + if cls._log_broker is not None: + cls.set_queue_handler(plugin_logger, cls._log_broker) + # GetLogger() resets the level to DEBUG, so re-apply the effective level. + plugin_logger.setLevel(cls._effective_plugin_log_level(plugin_name)) + return plugin_logger + @classmethod def set_queue_handler(cls, logger: logging.Logger, log_broker: LogBroker) -> None: - cls._ensure_logger_enricher_filter(logger) + cls._log_broker = log_broker - for handler in logger.handlers: - if isinstance(handler, LogQueueHandler): - return - - handler = LogQueueHandler(log_broker) - handler.setLevel(logging.DEBUG) - handler.addFilter(_QueueAnsiColorFilter()) - handler.setFormatter( - logging.Formatter( - "%(ansi_prefix)s[%(asctime)s.%(msecs)03d] %(plugin_tag)s [%(short_levelname)s]%(astrbot_version_tag)s " - "[%(source_file)s:%(source_line)d]: %(message)s%(ansi_reset)s", - datefmt="%Y-%m-%d %H:%M:%S", - ), + targets = [logger] + targets.extend( + logging.getLogger(f"{PLUGIN_LOGGER_PREFIX}{name}") + for name in cls._plugin_logger_names ) - logger.addHandler(handler) + for target in targets: + cls._ensure_logger_enricher_filter(target) + + if any(isinstance(handler, LogQueueHandler) for handler in target.handlers): + continue + + handler = LogQueueHandler(log_broker) + handler.setLevel(logging.DEBUG) + handler.addFilter(_QueueAnsiColorFilter()) + handler.setFormatter( + logging.Formatter( + "%(ansi_prefix)s[%(asctime)s.%(msecs)03d] %(plugin_tag)s [%(short_levelname)s]%(astrbot_version_tag)s " + "[%(source_file)s:%(source_line)d]: %(message)s%(ansi_reset)s", + datefmt="%Y-%m-%d %H:%M:%S", + ), + ) + target.addHandler(handler) @classmethod def _remove_sink(cls, sink_id: int | None) -> None: @@ -353,6 +473,22 @@ def configure_logger( except Exception: logger.setLevel(logging.INFO) + # Plugin loggers without an explicit override follow the global level. + plugin_level = logger.level + if not isinstance(plugin_level, int): + configured_level = logging.getLevelName(str(level).upper()) + plugin_level = ( + configured_level + if isinstance(configured_level, int) + else logging.INFO + ) + overrides = cls._load_plugin_level_overrides() + for name in cls._plugin_logger_names: + if name not in overrides: + logging.getLogger(f"{PLUGIN_LOGGER_PREFIX}{name}").setLevel( + plugin_level + ) + if "log_file" in config: file_conf = config.get("log_file") or {} enable_file = bool(file_conf.get("enable", False)) diff --git a/astrbot/core/star/base.py b/astrbot/core/star/base.py index b375abb829..91cf379b33 100644 --- a/astrbot/core/star/base.py +++ b/astrbot/core/star/base.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any from astrbot.core import html_renderer +from astrbot.core.log import LogManager from astrbot.core.utils.command_parser import CommandParserMixin from astrbot.core.utils.plugin_kv_store import PluginKVStoreMixin @@ -21,9 +22,34 @@ class Star(CommandParserMixin, PluginKVStoreMixin): author: str name: str context: Context + logger: logging.Logger + """The plugin's dedicated logger, isolated from the global ``astrbot`` logger.""" def __init__(self, context: Context, config: dict | None = None) -> None: self.context = context + # Resolve the plugin name from the metadata registered for this module + # first (it matches the name the dashboard uses); the loader also + # injects a sanitized ``name`` class attribute as a fallback. When both + # are absent (e.g. direct instantiation in tests), fall back to the + # global logger. + metadata = star_map.get(self.__class__.__module__) + plugin_name = (metadata.name if metadata else None) or getattr( + self, "name", None + ) + try: + self.logger = ( + LogManager.get_plugin_logger(plugin_name) + if plugin_name + else logging.getLogger("astrbot") + ) + logger.info( + "Plugin %s log level: %s.", + plugin_name or self.__class__.__name__, + logging.getLevelName(self.logger.getEffectiveLevel()), + ) + except AttributeError: + # The plugin defines ``logger`` as a read-only property; keep its own. + pass def _get_context_config(self) -> Any: get_config = getattr(self.context, "get_config", None) diff --git a/docs/zh/dev/star/plugin.md b/docs/zh/dev/star/plugin.md index 622e348ba4..b66047f914 100644 --- a/docs/zh/dev/star/plugin.md +++ b/docs/zh/dev/star/plugin.md @@ -93,6 +93,7 @@ class MyPlugin(Star): 3. 在 `__init__` 方法中会传入 `Context` 对象,这个对象包含了 AstrBot 的大多数组件 4. 具体的处理函数 `Handler` 在插件类中定义,如这里的 `helloworld` 函数。 5. 请务必使用 `from astrbot.api import logger` 来获取日志对象,而不是使用 `logging` 模块。 +6. 每个插件在 `__init__` 后会自动拥有独立的 `self.logger`。这个 logger 的等级可以在 WebUI 的插件配置弹窗中单独设置,不会影响其他插件和核心。也可以继续使用 `from astrbot.api import logger`,它会根据调用位置自动路由到当前插件的 logger。 > [!TIP] > diff --git a/tests/unit/test_api_logger.py b/tests/unit/test_api_logger.py new file mode 100644 index 0000000000..2b3d8e9694 --- /dev/null +++ b/tests/unit/test_api_logger.py @@ -0,0 +1,65 @@ +"""Tests for the plugin-aware logger exposed by ``astrbot.api``.""" + +import logging +from unittest.mock import MagicMock + +import pytest + +import astrbot.api as api +from astrbot.core.log import LogManager +from astrbot.core.star.star import StarMetadata, star_map + + +def test_plugin_logger_cache_refreshes_after_plugin_rename( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ensure a cached logger follows the latest plugin metadata name. + + Args: + monkeypatch: Pytest fixture used to isolate the plugin registry change. + """ + module_path = "data.plugins.cache_refresh.main" + caller_module = "data.plugins.cache_refresh.helpers" + monkeypatch.setitem( + star_map, + module_path, + StarMetadata(name="old_name", module_path=module_path), + ) + api._logger_cache.pop(caller_module, None) + + try: + old_logger = api._resolve_caller_logger(caller_module) + star_map[module_path] = StarMetadata( + name="new_name", + module_path=module_path, + ) + new_logger = api._resolve_caller_logger(caller_module) + + assert old_logger.name == "astrbot.plugin.old_name" + assert new_logger.name == "astrbot.plugin.new_name" + finally: + api._logger_cache.pop(caller_module, None) + + +def test_global_level_sync_handles_mock_logger( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ensure global level syncing always passes a valid logging level. + + Args: + monkeypatch: Pytest fixture used to isolate LogManager class state. + """ + plugin_name = "mock_level_sync" + plugin_logger = logging.getLogger(f"astrbot.plugin.{plugin_name}") + previous_level = plugin_logger.level + global_logger = MagicMock() + global_logger.level = MagicMock() + monkeypatch.setattr(LogManager, "_plugin_logger_names", {plugin_name}) + monkeypatch.setattr(LogManager, "_plugin_level_overrides", {}) + + try: + LogManager.configure_logger(global_logger, {"log_level": "WARNING"}) + + assert plugin_logger.level == logging.WARNING + finally: + plugin_logger.setLevel(previous_level) diff --git a/tests/unit/test_star_base.py b/tests/unit/test_star_base.py index 0a6f924c15..7e6df30779 100644 --- a/tests/unit/test_star_base.py +++ b/tests/unit/test_star_base.py @@ -1,5 +1,6 @@ """Tests for astrbot.core.star.base module.""" +import logging from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -31,6 +32,33 @@ class TestStar(Star): assert star.context is mock_context + def test_star_init_logs_plugin_level(self): + """Test that Star initialization reports the effective plugin level.""" + from astrbot.core.star import Star + + mock_context = MagicMock() + plugin_logger = MagicMock(spec=logging.Logger) + plugin_logger.getEffectiveLevel.return_value = logging.WARNING + + class TestLevelStar(Star): + name = "test_level_star" + author = "test_author" + + with ( + patch( + "astrbot.core.star.base.LogManager.get_plugin_logger", + return_value=plugin_logger, + ), + patch("astrbot.core.star.base.logger") as core_logger, + ): + TestLevelStar(context=mock_context) + + core_logger.info.assert_called_once_with( + "Plugin %s log level: %s.", + "test_level_star", + "WARNING", + ) + @pytest.mark.asyncio async def test_text_to_image_with_config(self): """Test text_to_image method with valid config.""" From 26702bd20e090df09c642bc4abdfa21cb15b0a8b Mon Sep 17 00:00:00 2001 From: RuochenPan Date: Tue, 21 Jul 2026 17:18:48 +0800 Subject: [PATCH 2/7] feat(dashboard): configure plugin log levels --- astrbot/dashboard/api/plugins.py | 37 +++++++++++++-- astrbot/dashboard/schemas.py | 5 +++ .../src/api/generated/openapi-v1/sdk.gen.ts | 13 +++++- .../src/api/generated/openapi-v1/types.gen.ts | 17 +++++++ dashboard/src/api/v1.ts | 11 +++++ .../locales/en-US/features/extension.json | 8 +++- .../locales/ru-RU/features/extension.json | 8 +++- .../locales/zh-CN/features/extension.json | 8 +++- dashboard/src/views/ExtensionPage.vue | 45 +++++++++++++++++-- .../src/views/extension/useExtensionPage.js | 38 ++++++++++++++++ openspec/openapi-v1.yaml | 26 +++++++++++ tests/test_fastapi_v1_dashboard.py | 11 +++++ 12 files changed, 217 insertions(+), 10 deletions(-) diff --git a/astrbot/dashboard/api/plugins.py b/astrbot/dashboard/api/plugins.py index 9291bd1199..8e257e68c2 100644 --- a/astrbot/dashboard/api/plugins.py +++ b/astrbot/dashboard/api/plugins.py @@ -10,12 +10,13 @@ from astrbot.api.web import PluginRequest, bind_request_context from astrbot.core import logger +from astrbot.core.log import LogManager from astrbot.dashboard.asgi_runtime import ( DashboardRequestState, call_request_view, ) from astrbot.dashboard.async_utils import run_maybe_async -from astrbot.dashboard.responses import ok +from astrbot.dashboard.responses import error, ok from astrbot.dashboard.schemas import ( EnabledPatch, PluginByIdRequest, @@ -24,6 +25,7 @@ PluginConfigUpdateRequest, PluginEnabledRequest, PluginInstallRequest, + PluginLogLevelPayload, PluginSourceBindRequest, PluginSourceRequest, PluginUninstallRequest, @@ -712,7 +714,13 @@ async def get_plugin_config_by_id( _auth: AuthContext = Depends(require_plugin_scope), service: ConfigDisplayService = Depends(get_config_display_service), ): - return ok({"plugin_name": plugin_id, **await service.get_configs(plugin_id)}) + return ok( + { + "plugin_name": plugin_id, + "log_level": LogManager.get_plugin_log_level(plugin_id), + **await service.get_configs(plugin_id), + } + ) @router.put("/plugins/config") @@ -956,7 +964,30 @@ async def get_plugin_config( _auth: AuthContext = Depends(require_plugin_scope), service: ConfigDisplayService = Depends(get_config_display_service), ): - return ok({"plugin_name": plugin_id, **await service.get_configs(plugin_id)}) + return ok( + { + "plugin_name": plugin_id, + "log_level": LogManager.get_plugin_log_level(plugin_id), + **await service.get_configs(plugin_id), + } + ) + + +@router.put("/plugins/{plugin_id}/log-level") +async def update_plugin_log_level( + plugin_id: str, + payload: PluginLogLevelPayload, + _auth: AuthContext = Depends(require_plugin_scope), +): + try: + LogManager.set_plugin_log_level(plugin_id, payload.level) + except ValueError as e: + return error(str(e)) + level_desc = payload.level.upper() if payload.level else "global" + return ok( + message=f"Log level of plugin {plugin_id} set to {level_desc}.", + data={"log_level": LogManager.get_plugin_log_level(plugin_id)}, + ) @router.put("/plugins/{plugin_id}/config") diff --git a/astrbot/dashboard/schemas.py b/astrbot/dashboard/schemas.py index 48a2cf6b32..6856ee9b97 100644 --- a/astrbot/dashboard/schemas.py +++ b/astrbot/dashboard/schemas.py @@ -589,6 +589,11 @@ class PluginConfigPayload(OpenModel): config: dict[str, Any] | None = None +class PluginLogLevelPayload(OpenModel): + level: str | None = None + """Log level name (DEBUG/INFO/WARNING/ERROR/CRITICAL), or null to follow the global level.""" + + class PluginSourceRequest(OpenModel): id: str | None = None name: str | None = None diff --git a/dashboard/src/api/generated/openapi-v1/sdk.gen.ts b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts index 9b7cc5ce59..12c50903f4 100644 --- a/dashboard/src/api/generated/openapi-v1/sdk.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import { createClient, createConfig, type OptionsLegacyParser, formDataBodySerializer } from '@hey-api/client-axios'; -import type { LoginData, LoginError, LoginResponse, LogoutError, LogoutResponse, GetAuthSetupStatusError, GetAuthSetupStatusResponse, SetupAuthData, SetupAuthError, SetupAuthResponse, SetupTotpData, SetupTotpError, SetupTotpResponse, RecoverTotpError, RecoverTotpResponse, UpdateAuthAccountData, UpdateAuthAccountError, UpdateAuthAccountResponse, ListApiKeysError, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyError, CreateApiKeyResponse, RevokeApiKeyData, RevokeApiKeyError, RevokeApiKeyResponse, DeleteApiKeyData, DeleteApiKeyError, DeleteApiKeyResponse, GetSystemConfigSchemaError, GetSystemConfigSchemaResponse, GetSystemConfigError, GetSystemConfigResponse, UpdateSystemConfigData, UpdateSystemConfigError, UpdateSystemConfigResponse, GetSystemConfigRuntimeError, GetSystemConfigRuntimeResponse, GetConfigProfileSchemaError, GetConfigProfileSchemaResponse, ListConfigProfilesError, ListConfigProfilesResponse, CreateConfigProfileData, CreateConfigProfileError, CreateConfigProfileResponse, GetConfigProfileData, GetConfigProfileError, GetConfigProfileResponse, UpdateConfigProfileContentData, UpdateConfigProfileContentError, UpdateConfigProfileContentResponse, RenameConfigProfileData, RenameConfigProfileError, RenameConfigProfileResponse, DeleteConfigProfileData, DeleteConfigProfileError, DeleteConfigProfileResponse, ListConfigRoutesError, ListConfigRoutesResponse, ReplaceConfigRoutesData, ReplaceConfigRoutesError, ReplaceConfigRoutesResponse, UpsertConfigRouteData, UpsertConfigRouteError, UpsertConfigRouteResponse, DeleteConfigRouteData, DeleteConfigRouteError, DeleteConfigRouteResponse, ListBotTypesError, ListBotTypesResponse, RegisterBotTypeData, RegisterBotTypeError, RegisterBotTypeResponse, ListBotsData, ListBotsError, ListBotsResponse, CreateBotData, CreateBotError, CreateBotResponse, ListBotStatsError, ListBotStatsResponse, GetBotByIdData, GetBotByIdError, GetBotByIdResponse, UpdateBotByIdData, UpdateBotByIdError, UpdateBotByIdResponse, DeleteBotByIdData, DeleteBotByIdError, DeleteBotByIdResponse, SetBotEnabledByIdData, SetBotEnabledByIdError, SetBotEnabledByIdResponse, TestBotByIdData, TestBotByIdError, TestBotByIdResponse, GetBotData, GetBotError, GetBotResponse, UpdateBotData, UpdateBotError, UpdateBotResponse, DeleteBotData, DeleteBotError, DeleteBotResponse, SetBotEnabledData, SetBotEnabledError, SetBotEnabledResponse, TestBotData, TestBotError, TestBotResponse, GetProviderSchemaError, GetProviderSchemaResponse, ListProviderSourcesError, ListProviderSourcesResponse, CreateProviderSourceData, CreateProviderSourceError, CreateProviderSourceResponse, GetProviderSourceByIdData, GetProviderSourceByIdError, GetProviderSourceByIdResponse, UpsertProviderSourceByIdData, UpsertProviderSourceByIdError, UpsertProviderSourceByIdResponse, DeleteProviderSourceByIdData, DeleteProviderSourceByIdError, DeleteProviderSourceByIdResponse, ListProviderSourceModelsByIdData, ListProviderSourceModelsByIdError, ListProviderSourceModelsByIdResponse, ListProvidersBySourceIdData, ListProvidersBySourceIdError, ListProvidersBySourceIdResponse, CreateProviderInSourceByIdData, CreateProviderInSourceByIdError, CreateProviderInSourceByIdResponse, GetProviderSourceData, GetProviderSourceError, GetProviderSourceResponse, UpsertProviderSourceData, UpsertProviderSourceError, UpsertProviderSourceResponse, DeleteProviderSourceData, DeleteProviderSourceError, DeleteProviderSourceResponse, ListProviderSourceModelsData, ListProviderSourceModelsError, ListProviderSourceModelsResponse, ListProvidersBySourceData, ListProvidersBySourceError, ListProvidersBySourceResponse, CreateProviderInSourceData, CreateProviderInSourceError, CreateProviderInSourceResponse, ListProvidersData, ListProvidersError, ListProvidersResponse, CreateProviderData, CreateProviderError, CreateProviderResponse, GetProviderByIdData, GetProviderByIdError, GetProviderByIdResponse, UpdateProviderByIdData, UpdateProviderByIdError, UpdateProviderByIdResponse, DeleteProviderByIdData, DeleteProviderByIdError, DeleteProviderByIdResponse, SetProviderEnabledByIdData, SetProviderEnabledByIdError, SetProviderEnabledByIdResponse, TestProviderByIdData, TestProviderByIdError, TestProviderByIdResponse, GetProviderEmbeddingDimensionByIdData, GetProviderEmbeddingDimensionByIdError, GetProviderEmbeddingDimensionByIdResponse, GetProviderData, GetProviderError, GetProviderResponse, UpdateProviderData, UpdateProviderError, UpdateProviderResponse, DeleteProviderData, DeleteProviderError, DeleteProviderResponse, SetProviderEnabledData, SetProviderEnabledError, SetProviderEnabledResponse, TestProviderData, TestProviderError, TestProviderResponse, GetProviderEmbeddingDimensionData, GetProviderEmbeddingDimensionError, GetProviderEmbeddingDimensionResponse, SendChatMessageData, SendChatMessageError, SendChatMessageResponse, OpenChatWebSocketData, OpenLiveChatWebSocketData, OpenUnifiedChatWebSocketData, ListChatSessionsData, ListChatSessionsError, ListChatSessionsResponse, CreateChatSessionData, CreateChatSessionError, CreateChatSessionResponse, BatchDeleteChatSessionsData, BatchDeleteChatSessionsError, BatchDeleteChatSessionsResponse, GetChatSessionData, GetChatSessionError, GetChatSessionResponse, UpdateChatSessionData, UpdateChatSessionError, UpdateChatSessionResponse, DeleteChatSessionData, DeleteChatSessionError, DeleteChatSessionResponse, StopChatSessionData, StopChatSessionError, StopChatSessionResponse, ResumeChatRunData, ResumeChatRunError, ResumeChatRunResponse, UpdateChatMessageData, UpdateChatMessageError, UpdateChatMessageResponse, RegenerateChatMessageData, RegenerateChatMessageError, RegenerateChatMessageResponse, ListChatConfigsError, ListChatConfigsResponse, CreateChatThreadData, CreateChatThreadError, CreateChatThreadResponse, GetChatThreadData, GetChatThreadError, GetChatThreadResponse, DeleteChatThreadData, DeleteChatThreadError, DeleteChatThreadResponse, SendChatThreadMessageData, SendChatThreadMessageError, SendChatThreadMessageResponse, ListChatProjectsError, ListChatProjectsResponse, CreateChatProjectData, CreateChatProjectError, CreateChatProjectResponse, GetChatProjectData, GetChatProjectError, GetChatProjectResponse, UpdateChatProjectData, UpdateChatProjectError, UpdateChatProjectResponse, DeleteChatProjectData, DeleteChatProjectError, DeleteChatProjectResponse, ListChatProjectSessionsData, ListChatProjectSessionsError, ListChatProjectSessionsResponse, AddChatProjectSessionData, AddChatProjectSessionError, AddChatProjectSessionResponse, RemoveChatProjectSessionData, RemoveChatProjectSessionError, RemoveChatProjectSessionResponse, SendImMessageData, SendImMessageError, SendImMessageResponse, ListImBotsError, ListImBotsResponse, UploadFileData, UploadFileError, UploadFileResponse, UploadOpenApiFileData, UploadOpenApiFileError, UploadOpenApiFileResponse, DownloadOpenApiFileData, DownloadOpenApiFileError, DownloadOpenApiFileResponse, GetFileByNameData, GetFileByNameError, GetFileByNameResponse, GetTokenFileData, GetTokenFileError, GetTokenFileResponse, GetAttachmentData, GetAttachmentError, GetAttachmentResponse, DeleteAttachmentData, DeleteAttachmentError, DeleteAttachmentResponse, DownloadAttachmentData, DownloadAttachmentError, DownloadAttachmentResponse, ListPluginsData, ListPluginsError, ListPluginsResponse, GetPluginByIdData, GetPluginByIdError, GetPluginByIdResponse, UninstallPluginByIdData, UninstallPluginByIdError, UninstallPluginByIdResponse, GetPluginConfigByIdData, GetPluginConfigByIdError, GetPluginConfigByIdResponse, UpdatePluginConfigByIdData, UpdatePluginConfigByIdError, UpdatePluginConfigByIdResponse, GetPluginConfigSchemaByIdData, GetPluginConfigSchemaByIdError, GetPluginConfigSchemaByIdResponse, ListPluginConfigFilesByIdData, ListPluginConfigFilesByIdError, ListPluginConfigFilesByIdResponse, UploadPluginConfigFilesByIdData, UploadPluginConfigFilesByIdError, UploadPluginConfigFilesByIdResponse, DeletePluginConfigFileByIdData, DeletePluginConfigFileByIdError, DeletePluginConfigFileByIdResponse, GetPluginReadmeByIdData, GetPluginReadmeByIdError, GetPluginReadmeByIdResponse, GetPluginChangelogByIdData, GetPluginChangelogByIdError, GetPluginChangelogByIdResponse, ReloadPluginByIdData, ReloadPluginByIdError, ReloadPluginByIdResponse, SetPluginEnabledByIdData, SetPluginEnabledByIdError, SetPluginEnabledByIdResponse, ListPluginPagesByIdData, ListPluginPagesByIdError, ListPluginPagesByIdResponse, GetPluginPageByIdData, GetPluginPageByIdError, GetPluginPageByIdResponse, GetPluginPageAssetByIdData, GetPluginPageAssetByIdError, GetPluginPageAssetByIdResponse, GetPluginData, GetPluginError, GetPluginResponse, UninstallPluginData, UninstallPluginError, UninstallPluginResponse, GetPluginConfigData, GetPluginConfigError, GetPluginConfigResponse, UpdatePluginConfigData, UpdatePluginConfigError, UpdatePluginConfigResponse, GetPluginConfigSchemaData, GetPluginConfigSchemaError, GetPluginConfigSchemaResponse, ListPluginConfigFilesData, ListPluginConfigFilesError, ListPluginConfigFilesResponse, UploadPluginConfigFilesData, UploadPluginConfigFilesError, UploadPluginConfigFilesResponse, DeletePluginConfigFileData, DeletePluginConfigFileError, DeletePluginConfigFileResponse, GetPluginReadmeData, GetPluginReadmeError, GetPluginReadmeResponse, GetPluginChangelogData, GetPluginChangelogError, GetPluginChangelogResponse, ReloadPluginData, ReloadPluginError, ReloadPluginResponse, BindPluginSourceData, BindPluginSourceError, BindPluginSourceResponse, SetPluginEnabledData, SetPluginEnabledError, SetPluginEnabledResponse, UpdatePluginData, UpdatePluginError, UpdatePluginResponse, UpdatePluginsData, UpdatePluginsError, UpdatePluginsResponse, CheckPluginVersionSupportData, CheckPluginVersionSupportError, CheckPluginVersionSupportResponse, ValidatePluginRepoData, ValidatePluginRepoError, ValidatePluginRepoResponse, ListFailedPluginsError, ListFailedPluginsResponse, UninstallFailedPluginData, UninstallFailedPluginError, UninstallFailedPluginResponse, ReloadFailedPluginData, ReloadFailedPluginError, ReloadFailedPluginResponse, InstallPluginFromGithubData, InstallPluginFromGithubError, InstallPluginFromGithubResponse, InstallPluginFromUrlData, InstallPluginFromUrlError, InstallPluginFromUrlResponse, InstallPluginFromUploadData, InstallPluginFromUploadError, InstallPluginFromUploadResponse, ListPluginMarketData, ListPluginMarketError, ListPluginMarketResponse, ListPluginMarketCategoriesError, ListPluginMarketCategoriesResponse, ListPluginSourcesError, ListPluginSourcesResponse, CreatePluginSourceData, CreatePluginSourceError, CreatePluginSourceResponse, ReplacePluginSourcesData, ReplacePluginSourcesError, ReplacePluginSourcesResponse, DeletePluginSourceData, DeletePluginSourceError, DeletePluginSourceResponse, DeletePluginSourceByIdData, DeletePluginSourceByIdError, DeletePluginSourceByIdResponse, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginPageAssetData, GetPluginPageAssetError, GetPluginPageAssetResponse, GetPluginPageBridgeSdkError, GetPluginPageBridgeSdkResponse, GetPluginExtensionRouteData, GetPluginExtensionRouteError, GetPluginExtensionRouteResponse, PostPluginExtensionRouteData, PostPluginExtensionRouteError, PostPluginExtensionRouteResponse, PutPluginExtensionRouteData, PutPluginExtensionRouteError, PutPluginExtensionRouteResponse, PatchPluginExtensionRouteData, PatchPluginExtensionRouteError, PatchPluginExtensionRouteResponse, DeletePluginExtensionRouteData, DeletePluginExtensionRouteError, DeletePluginExtensionRouteResponse, ListCommandsData, ListCommandsError, ListCommandsResponse, UpdateCommandData, UpdateCommandError, UpdateCommandResponse, ListCommandConflictsError, ListCommandConflictsResponse, ListToolsData, ListToolsError, ListToolsResponse, SetToolEnabledData, SetToolEnabledError, SetToolEnabledResponse, SetToolPermissionData, SetToolPermissionError, SetToolPermissionResponse, ListMcpServersError, ListMcpServersResponse, CreateMcpServerData, CreateMcpServerError, CreateMcpServerResponse, UpdateMcpServerByNameData, UpdateMcpServerByNameError, UpdateMcpServerByNameResponse, DeleteMcpServerByNameData, DeleteMcpServerByNameError, DeleteMcpServerByNameResponse, SetMcpServerEnabledByNameData, SetMcpServerEnabledByNameError, SetMcpServerEnabledByNameResponse, TestMcpServerByNameData, TestMcpServerByNameError, TestMcpServerByNameResponse, UpdateMcpServerData, UpdateMcpServerError, UpdateMcpServerResponse, DeleteMcpServerData, DeleteMcpServerError, DeleteMcpServerResponse, SetMcpServerEnabledData, SetMcpServerEnabledError, SetMcpServerEnabledResponse, TestMcpServerData, TestMcpServerError, TestMcpServerResponse, SyncModelScopeMcpServersData, SyncModelScopeMcpServersError, SyncModelScopeMcpServersResponse, ListSkillsData, ListSkillsError, ListSkillsResponse, UploadSkillData, UploadSkillError, UploadSkillResponse, UploadSkillsBatchData, UploadSkillsBatchError, UploadSkillsBatchResponse, UpdateSkillByNameData, UpdateSkillByNameError, UpdateSkillByNameResponse, DeleteSkillByNameData, DeleteSkillByNameError, DeleteSkillByNameResponse, DownloadSkillByNameData, DownloadSkillByNameError, DownloadSkillByNameResponse, ListSkillFilesByNameData, ListSkillFilesByNameError, ListSkillFilesByNameResponse, GetSkillFileByNameData, GetSkillFileByNameError, GetSkillFileByNameResponse, UpdateSkillFileByNameData, UpdateSkillFileByNameError, UpdateSkillFileByNameResponse, UpdateSkillData, UpdateSkillError, UpdateSkillResponse, DeleteSkillData, DeleteSkillError, DeleteSkillResponse, DownloadSkillData, DownloadSkillError, DownloadSkillResponse, ListSkillFilesData, ListSkillFilesError, ListSkillFilesResponse, GetSkillFileData, GetSkillFileError, GetSkillFileResponse, UpdateSkillFileData, UpdateSkillFileError, UpdateSkillFileResponse, ListNeoSkillCandidatesData, ListNeoSkillCandidatesError, ListNeoSkillCandidatesResponse, ListNeoSkillReleasesData, ListNeoSkillReleasesError, ListNeoSkillReleasesResponse, GetNeoSkillPayloadData, GetNeoSkillPayloadError, GetNeoSkillPayloadResponse, EvaluateNeoSkillCandidateData, EvaluateNeoSkillCandidateError, EvaluateNeoSkillCandidateResponse, PromoteNeoSkillCandidateData, PromoteNeoSkillCandidateError, PromoteNeoSkillCandidateResponse, RollbackNeoSkillReleaseData, RollbackNeoSkillReleaseError, RollbackNeoSkillReleaseResponse, SyncNeoSkillReleaseData, SyncNeoSkillReleaseError, SyncNeoSkillReleaseResponse, DeleteNeoSkillCandidateData, DeleteNeoSkillCandidateError, DeleteNeoSkillCandidateResponse, DeleteNeoSkillReleaseData, DeleteNeoSkillReleaseError, DeleteNeoSkillReleaseResponse, ListKnowledgeBasesData, ListKnowledgeBasesError, ListKnowledgeBasesResponse, CreateKnowledgeBaseData, CreateKnowledgeBaseError, CreateKnowledgeBaseResponse, GetKnowledgeBaseData, GetKnowledgeBaseError, GetKnowledgeBaseResponse, UpdateKnowledgeBaseData, UpdateKnowledgeBaseError, UpdateKnowledgeBaseResponse, DeleteKnowledgeBaseData, DeleteKnowledgeBaseError, DeleteKnowledgeBaseResponse, GetKnowledgeBaseStatsData, GetKnowledgeBaseStatsError, GetKnowledgeBaseStatsResponse, ListKnowledgeDocumentsData, ListKnowledgeDocumentsError, ListKnowledgeDocumentsResponse, UploadKnowledgeDocumentData, UploadKnowledgeDocumentError, UploadKnowledgeDocumentResponse, ImportKnowledgeDocumentsData, ImportKnowledgeDocumentsError, ImportKnowledgeDocumentsResponse, ImportKnowledgeDocumentFromUrlData, ImportKnowledgeDocumentFromUrlError, ImportKnowledgeDocumentFromUrlResponse, GetKnowledgeDocumentData, GetKnowledgeDocumentError, GetKnowledgeDocumentResponse, DeleteKnowledgeDocumentData, DeleteKnowledgeDocumentError, DeleteKnowledgeDocumentResponse, ListKnowledgeChunksData, ListKnowledgeChunksError, ListKnowledgeChunksResponse, DeleteKnowledgeChunkData, DeleteKnowledgeChunkError, DeleteKnowledgeChunkResponse, RetrieveKnowledgeBaseData, RetrieveKnowledgeBaseError, RetrieveKnowledgeBaseResponse, GetKnowledgeTaskData, GetKnowledgeTaskError, GetKnowledgeTaskResponse, GetPersonaTreeError, GetPersonaTreeResponse, ListPersonasData, ListPersonasError, ListPersonasResponse, CreatePersonaData, CreatePersonaError, CreatePersonaResponse, GetPersonaByIdData, GetPersonaByIdError, GetPersonaByIdResponse, UpdatePersonaByIdData, UpdatePersonaByIdError, UpdatePersonaByIdResponse, DeletePersonaByIdData, DeletePersonaByIdError, DeletePersonaByIdResponse, GetPersonaData, GetPersonaError, GetPersonaResponse, UpdatePersonaData, UpdatePersonaError, UpdatePersonaResponse, DeletePersonaData, DeletePersonaError, DeletePersonaResponse, ListPersonaFoldersData, ListPersonaFoldersError, ListPersonaFoldersResponse, CreatePersonaFolderData, CreatePersonaFolderError, CreatePersonaFolderResponse, UpdatePersonaFolderData, UpdatePersonaFolderError, UpdatePersonaFolderResponse, DeletePersonaFolderData, DeletePersonaFolderError, DeletePersonaFolderResponse, MovePersonaItemData, MovePersonaItemError, MovePersonaItemResponse, ReorderPersonaItemsData, ReorderPersonaItemsError, ReorderPersonaItemsResponse, ListSessionsData, ListSessionsError, ListSessionsResponse, ListActiveUmosError, ListActiveUmosResponse, ListSessionRulesData, ListSessionRulesError, ListSessionRulesResponse, UpsertSessionRuleData, UpsertSessionRuleError, UpsertSessionRuleResponse, DeleteSessionRulesData, DeleteSessionRulesError, DeleteSessionRulesResponse, BatchUpdateSessionProviderData, BatchUpdateSessionProviderError, BatchUpdateSessionProviderResponse, BatchUpdateSessionServiceData, BatchUpdateSessionServiceError, BatchUpdateSessionServiceResponse, ListSessionGroupsError, ListSessionGroupsResponse, CreateSessionGroupData, CreateSessionGroupError, CreateSessionGroupResponse, UpdateSessionGroupData, UpdateSessionGroupError, UpdateSessionGroupResponse, DeleteSessionGroupData, DeleteSessionGroupError, DeleteSessionGroupResponse, ListConversationsData, ListConversationsError, ListConversationsResponse, BatchDeleteConversationsData, BatchDeleteConversationsError, BatchDeleteConversationsResponse, GetConversationData, GetConversationError, GetConversationResponse, UpdateConversationData, UpdateConversationError, UpdateConversationResponse, DeleteConversationData, DeleteConversationError, DeleteConversationResponse, ReplaceConversationMessagesData, ReplaceConversationMessagesError, ReplaceConversationMessagesResponse, ExportConversationsData, ExportConversationsError, ExportConversationsResponse, GetStatsData, GetStatsError, GetStatsResponse, GetProviderTokenStatsData, GetProviderTokenStatsError, GetProviderTokenStatsResponse, GetVersionError, GetVersionResponse, GetPublicVersionsError, GetPublicVersionsResponse, GetFirstNoticeData, GetFirstNoticeError, GetFirstNoticeResponse, TestGhproxyConnectionData, TestGhproxyConnectionError, TestGhproxyConnectionResponse, ListChangelogVersionsError, ListChangelogVersionsResponse, GetChangelogData, GetChangelogError, GetChangelogResponse, GetStartTimeError, GetStartTimeResponse, GetStorageStatusError, GetStorageStatusResponse, CleanupStorageData, CleanupStorageError, CleanupStorageResponse, RestartCoreError, RestartCoreResponse, ListBackupsData, ListBackupsError, ListBackupsResponse, CreateBackupData, CreateBackupError, CreateBackupResponse, UploadBackupData, UploadBackupError, UploadBackupResponse, InitBackupUploadData, InitBackupUploadError, InitBackupUploadResponse, UploadBackupChunkData, UploadBackupChunkError, UploadBackupChunkResponse, CompleteBackupUploadData, CompleteBackupUploadError, CompleteBackupUploadResponse, AbortBackupUploadData, AbortBackupUploadError, AbortBackupUploadResponse, GetBackupProgressData, GetBackupProgressError, GetBackupProgressResponse, DownloadBackupData, DownloadBackupError, DownloadBackupResponse, RenameBackupData, RenameBackupError, RenameBackupResponse, DeleteBackupData, DeleteBackupError, DeleteBackupResponse, CheckBackupData, CheckBackupError, CheckBackupResponse, ImportBackupData, ImportBackupError, ImportBackupResponse, CheckUpdateError, CheckUpdateResponse, ListReleasesData, ListReleasesError, ListReleasesResponse, UpdateCoreData, UpdateCoreError, UpdateCoreResponse, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, GetUpdateProgressData, GetUpdateProgressError, GetUpdateProgressResponse, InstallPipPackageData, InstallPipPackageError, InstallPipPackageResponse, ListCronJobsData, ListCronJobsError, ListCronJobsResponse, CreateCronJobData, CreateCronJobError, CreateCronJobResponse, UpdateCronJobData, UpdateCronJobError, UpdateCronJobResponse, DeleteCronJobData, DeleteCronJobError, DeleteCronJobResponse, RunCronJobData, RunCronJobError, RunCronJobResponse, StreamLiveLogsError, StreamLiveLogsResponse, GetLogHistoryError, GetLogHistoryResponse, GetTraceSettingsError, GetTraceSettingsResponse, UpdateTraceSettingsData, UpdateTraceSettingsError, UpdateTraceSettingsResponse, ListT2iTemplatesError, ListT2iTemplatesResponse, CreateT2iTemplateData, CreateT2iTemplateError, CreateT2iTemplateResponse, GetActiveT2iTemplateError, GetActiveT2iTemplateResponse, SetActiveT2iTemplateData, SetActiveT2iTemplateError, SetActiveT2iTemplateResponse, ResetDefaultT2iTemplateError, ResetDefaultT2iTemplateResponse, GetT2iTemplateData, GetT2iTemplateError, GetT2iTemplateResponse, UpdateT2iTemplateData, UpdateT2iTemplateError, UpdateT2iTemplateResponse, DeleteT2iTemplateData, DeleteT2iTemplateError, DeleteT2iTemplateResponse, GetSubagentConfigError, GetSubagentConfigResponse, UpdateSubagentConfigData, UpdateSubagentConfigError, UpdateSubagentConfigResponse, ListSubagentAvailableToolsError, ListSubagentAvailableToolsResponse, VerifyPlatformWebhookData, VerifyPlatformWebhookError, VerifyPlatformWebhookResponse, ReceivePlatformWebhookData, ReceivePlatformWebhookError, ReceivePlatformWebhookResponse } from './types.gen'; +import type { LoginData, LoginError, LoginResponse, LogoutError, LogoutResponse, GetAuthSetupStatusError, GetAuthSetupStatusResponse, SetupAuthData, SetupAuthError, SetupAuthResponse, SetupTotpData, SetupTotpError, SetupTotpResponse, RecoverTotpError, RecoverTotpResponse, UpdateAuthAccountData, UpdateAuthAccountError, UpdateAuthAccountResponse, ListApiKeysError, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyError, CreateApiKeyResponse, RevokeApiKeyData, RevokeApiKeyError, RevokeApiKeyResponse, DeleteApiKeyData, DeleteApiKeyError, DeleteApiKeyResponse, GetSystemConfigSchemaError, GetSystemConfigSchemaResponse, GetSystemConfigError, GetSystemConfigResponse, UpdateSystemConfigData, UpdateSystemConfigError, UpdateSystemConfigResponse, GetSystemConfigRuntimeError, GetSystemConfigRuntimeResponse, GetConfigProfileSchemaError, GetConfigProfileSchemaResponse, ListConfigProfilesError, ListConfigProfilesResponse, CreateConfigProfileData, CreateConfigProfileError, CreateConfigProfileResponse, GetConfigProfileData, GetConfigProfileError, GetConfigProfileResponse, UpdateConfigProfileContentData, UpdateConfigProfileContentError, UpdateConfigProfileContentResponse, RenameConfigProfileData, RenameConfigProfileError, RenameConfigProfileResponse, DeleteConfigProfileData, DeleteConfigProfileError, DeleteConfigProfileResponse, ListConfigRoutesError, ListConfigRoutesResponse, ReplaceConfigRoutesData, ReplaceConfigRoutesError, ReplaceConfigRoutesResponse, UpsertConfigRouteData, UpsertConfigRouteError, UpsertConfigRouteResponse, DeleteConfigRouteData, DeleteConfigRouteError, DeleteConfigRouteResponse, ListBotTypesError, ListBotTypesResponse, RegisterBotTypeData, RegisterBotTypeError, RegisterBotTypeResponse, ListBotsData, ListBotsError, ListBotsResponse, CreateBotData, CreateBotError, CreateBotResponse, ListBotStatsError, ListBotStatsResponse, GetBotByIdData, GetBotByIdError, GetBotByIdResponse, UpdateBotByIdData, UpdateBotByIdError, UpdateBotByIdResponse, DeleteBotByIdData, DeleteBotByIdError, DeleteBotByIdResponse, SetBotEnabledByIdData, SetBotEnabledByIdError, SetBotEnabledByIdResponse, TestBotByIdData, TestBotByIdError, TestBotByIdResponse, GetBotData, GetBotError, GetBotResponse, UpdateBotData, UpdateBotError, UpdateBotResponse, DeleteBotData, DeleteBotError, DeleteBotResponse, SetBotEnabledData, SetBotEnabledError, SetBotEnabledResponse, TestBotData, TestBotError, TestBotResponse, GetProviderSchemaError, GetProviderSchemaResponse, ListProviderSourcesError, ListProviderSourcesResponse, CreateProviderSourceData, CreateProviderSourceError, CreateProviderSourceResponse, GetProviderSourceByIdData, GetProviderSourceByIdError, GetProviderSourceByIdResponse, UpsertProviderSourceByIdData, UpsertProviderSourceByIdError, UpsertProviderSourceByIdResponse, DeleteProviderSourceByIdData, DeleteProviderSourceByIdError, DeleteProviderSourceByIdResponse, ListProviderSourceModelsByIdData, ListProviderSourceModelsByIdError, ListProviderSourceModelsByIdResponse, ListProvidersBySourceIdData, ListProvidersBySourceIdError, ListProvidersBySourceIdResponse, CreateProviderInSourceByIdData, CreateProviderInSourceByIdError, CreateProviderInSourceByIdResponse, GetProviderSourceData, GetProviderSourceError, GetProviderSourceResponse, UpsertProviderSourceData, UpsertProviderSourceError, UpsertProviderSourceResponse, DeleteProviderSourceData, DeleteProviderSourceError, DeleteProviderSourceResponse, ListProviderSourceModelsData, ListProviderSourceModelsError, ListProviderSourceModelsResponse, ListProvidersBySourceData, ListProvidersBySourceError, ListProvidersBySourceResponse, CreateProviderInSourceData, CreateProviderInSourceError, CreateProviderInSourceResponse, ListProvidersData, ListProvidersError, ListProvidersResponse, CreateProviderData, CreateProviderError, CreateProviderResponse, GetProviderByIdData, GetProviderByIdError, GetProviderByIdResponse, UpdateProviderByIdData, UpdateProviderByIdError, UpdateProviderByIdResponse, DeleteProviderByIdData, DeleteProviderByIdError, DeleteProviderByIdResponse, SetProviderEnabledByIdData, SetProviderEnabledByIdError, SetProviderEnabledByIdResponse, TestProviderByIdData, TestProviderByIdError, TestProviderByIdResponse, GetProviderEmbeddingDimensionByIdData, GetProviderEmbeddingDimensionByIdError, GetProviderEmbeddingDimensionByIdResponse, GetProviderData, GetProviderError, GetProviderResponse, UpdateProviderData, UpdateProviderError, UpdateProviderResponse, DeleteProviderData, DeleteProviderError, DeleteProviderResponse, SetProviderEnabledData, SetProviderEnabledError, SetProviderEnabledResponse, TestProviderData, TestProviderError, TestProviderResponse, GetProviderEmbeddingDimensionData, GetProviderEmbeddingDimensionError, GetProviderEmbeddingDimensionResponse, SendChatMessageData, SendChatMessageError, SendChatMessageResponse, OpenChatWebSocketData, OpenLiveChatWebSocketData, OpenUnifiedChatWebSocketData, ListChatSessionsData, ListChatSessionsError, ListChatSessionsResponse, CreateChatSessionData, CreateChatSessionError, CreateChatSessionResponse, BatchDeleteChatSessionsData, BatchDeleteChatSessionsError, BatchDeleteChatSessionsResponse, GetChatSessionData, GetChatSessionError, GetChatSessionResponse, UpdateChatSessionData, UpdateChatSessionError, UpdateChatSessionResponse, DeleteChatSessionData, DeleteChatSessionError, DeleteChatSessionResponse, StopChatSessionData, StopChatSessionError, StopChatSessionResponse, ResumeChatRunData, ResumeChatRunError, ResumeChatRunResponse, UpdateChatMessageData, UpdateChatMessageError, UpdateChatMessageResponse, RegenerateChatMessageData, RegenerateChatMessageError, RegenerateChatMessageResponse, ListChatConfigsError, ListChatConfigsResponse, CreateChatThreadData, CreateChatThreadError, CreateChatThreadResponse, GetChatThreadData, GetChatThreadError, GetChatThreadResponse, DeleteChatThreadData, DeleteChatThreadError, DeleteChatThreadResponse, SendChatThreadMessageData, SendChatThreadMessageError, SendChatThreadMessageResponse, ListChatProjectsError, ListChatProjectsResponse, CreateChatProjectData, CreateChatProjectError, CreateChatProjectResponse, GetChatProjectData, GetChatProjectError, GetChatProjectResponse, UpdateChatProjectData, UpdateChatProjectError, UpdateChatProjectResponse, DeleteChatProjectData, DeleteChatProjectError, DeleteChatProjectResponse, ListChatProjectSessionsData, ListChatProjectSessionsError, ListChatProjectSessionsResponse, AddChatProjectSessionData, AddChatProjectSessionError, AddChatProjectSessionResponse, RemoveChatProjectSessionData, RemoveChatProjectSessionError, RemoveChatProjectSessionResponse, SendImMessageData, SendImMessageError, SendImMessageResponse, ListImBotsError, ListImBotsResponse, UploadFileData, UploadFileError, UploadFileResponse, UploadOpenApiFileData, UploadOpenApiFileError, UploadOpenApiFileResponse, DownloadOpenApiFileData, DownloadOpenApiFileError, DownloadOpenApiFileResponse, GetFileByNameData, GetFileByNameError, GetFileByNameResponse, GetTokenFileData, GetTokenFileError, GetTokenFileResponse, GetAttachmentData, GetAttachmentError, GetAttachmentResponse, DeleteAttachmentData, DeleteAttachmentError, DeleteAttachmentResponse, DownloadAttachmentData, DownloadAttachmentError, DownloadAttachmentResponse, ListPluginsData, ListPluginsError, ListPluginsResponse, GetPluginByIdData, GetPluginByIdError, GetPluginByIdResponse, UninstallPluginByIdData, UninstallPluginByIdError, UninstallPluginByIdResponse, GetPluginConfigByIdData, GetPluginConfigByIdError, GetPluginConfigByIdResponse, UpdatePluginConfigByIdData, UpdatePluginConfigByIdError, UpdatePluginConfigByIdResponse, GetPluginConfigSchemaByIdData, GetPluginConfigSchemaByIdError, GetPluginConfigSchemaByIdResponse, ListPluginConfigFilesByIdData, ListPluginConfigFilesByIdError, ListPluginConfigFilesByIdResponse, UploadPluginConfigFilesByIdData, UploadPluginConfigFilesByIdError, UploadPluginConfigFilesByIdResponse, DeletePluginConfigFileByIdData, DeletePluginConfigFileByIdError, DeletePluginConfigFileByIdResponse, GetPluginReadmeByIdData, GetPluginReadmeByIdError, GetPluginReadmeByIdResponse, GetPluginChangelogByIdData, GetPluginChangelogByIdError, GetPluginChangelogByIdResponse, ReloadPluginByIdData, ReloadPluginByIdError, ReloadPluginByIdResponse, SetPluginEnabledByIdData, SetPluginEnabledByIdError, SetPluginEnabledByIdResponse, ListPluginPagesByIdData, ListPluginPagesByIdError, ListPluginPagesByIdResponse, GetPluginPageByIdData, GetPluginPageByIdError, GetPluginPageByIdResponse, GetPluginPageAssetByIdData, GetPluginPageAssetByIdError, GetPluginPageAssetByIdResponse, GetPluginData, GetPluginError, GetPluginResponse, UninstallPluginData, UninstallPluginError, UninstallPluginResponse, GetPluginConfigData, GetPluginConfigError, GetPluginConfigResponse, UpdatePluginConfigData, UpdatePluginConfigError, UpdatePluginConfigResponse, UpdatePluginLogLevelData, UpdatePluginLogLevelError, UpdatePluginLogLevelResponse, GetPluginConfigSchemaData, GetPluginConfigSchemaError, GetPluginConfigSchemaResponse, ListPluginConfigFilesData, ListPluginConfigFilesError, ListPluginConfigFilesResponse, UploadPluginConfigFilesData, UploadPluginConfigFilesError, UploadPluginConfigFilesResponse, DeletePluginConfigFileData, DeletePluginConfigFileError, DeletePluginConfigFileResponse, GetPluginReadmeData, GetPluginReadmeError, GetPluginReadmeResponse, GetPluginChangelogData, GetPluginChangelogError, GetPluginChangelogResponse, ReloadPluginData, ReloadPluginError, ReloadPluginResponse, BindPluginSourceData, BindPluginSourceError, BindPluginSourceResponse, SetPluginEnabledData, SetPluginEnabledError, SetPluginEnabledResponse, UpdatePluginData, UpdatePluginError, UpdatePluginResponse, UpdatePluginsData, UpdatePluginsError, UpdatePluginsResponse, CheckPluginVersionSupportData, CheckPluginVersionSupportError, CheckPluginVersionSupportResponse, ValidatePluginRepoData, ValidatePluginRepoError, ValidatePluginRepoResponse, ListFailedPluginsError, ListFailedPluginsResponse, UninstallFailedPluginData, UninstallFailedPluginError, UninstallFailedPluginResponse, ReloadFailedPluginData, ReloadFailedPluginError, ReloadFailedPluginResponse, InstallPluginFromGithubData, InstallPluginFromGithubError, InstallPluginFromGithubResponse, InstallPluginFromUrlData, InstallPluginFromUrlError, InstallPluginFromUrlResponse, InstallPluginFromUploadData, InstallPluginFromUploadError, InstallPluginFromUploadResponse, ListPluginMarketData, ListPluginMarketError, ListPluginMarketResponse, ListPluginMarketCategoriesError, ListPluginMarketCategoriesResponse, ListPluginSourcesError, ListPluginSourcesResponse, CreatePluginSourceData, CreatePluginSourceError, CreatePluginSourceResponse, ReplacePluginSourcesData, ReplacePluginSourcesError, ReplacePluginSourcesResponse, DeletePluginSourceData, DeletePluginSourceError, DeletePluginSourceResponse, DeletePluginSourceByIdData, DeletePluginSourceByIdError, DeletePluginSourceByIdResponse, ListPluginPagesData, ListPluginPagesError, ListPluginPagesResponse, GetPluginPageData, GetPluginPageError, GetPluginPageResponse, GetPluginPageAssetData, GetPluginPageAssetError, GetPluginPageAssetResponse, GetPluginPageBridgeSdkError, GetPluginPageBridgeSdkResponse, GetPluginExtensionRouteData, GetPluginExtensionRouteError, GetPluginExtensionRouteResponse, PostPluginExtensionRouteData, PostPluginExtensionRouteError, PostPluginExtensionRouteResponse, PutPluginExtensionRouteData, PutPluginExtensionRouteError, PutPluginExtensionRouteResponse, PatchPluginExtensionRouteData, PatchPluginExtensionRouteError, PatchPluginExtensionRouteResponse, DeletePluginExtensionRouteData, DeletePluginExtensionRouteError, DeletePluginExtensionRouteResponse, ListCommandsData, ListCommandsError, ListCommandsResponse, UpdateCommandData, UpdateCommandError, UpdateCommandResponse, ListCommandConflictsError, ListCommandConflictsResponse, ListToolsData, ListToolsError, ListToolsResponse, SetToolEnabledData, SetToolEnabledError, SetToolEnabledResponse, SetToolPermissionData, SetToolPermissionError, SetToolPermissionResponse, ListMcpServersError, ListMcpServersResponse, CreateMcpServerData, CreateMcpServerError, CreateMcpServerResponse, UpdateMcpServerByNameData, UpdateMcpServerByNameError, UpdateMcpServerByNameResponse, DeleteMcpServerByNameData, DeleteMcpServerByNameError, DeleteMcpServerByNameResponse, SetMcpServerEnabledByNameData, SetMcpServerEnabledByNameError, SetMcpServerEnabledByNameResponse, TestMcpServerByNameData, TestMcpServerByNameError, TestMcpServerByNameResponse, UpdateMcpServerData, UpdateMcpServerError, UpdateMcpServerResponse, DeleteMcpServerData, DeleteMcpServerError, DeleteMcpServerResponse, SetMcpServerEnabledData, SetMcpServerEnabledError, SetMcpServerEnabledResponse, TestMcpServerData, TestMcpServerError, TestMcpServerResponse, SyncModelScopeMcpServersData, SyncModelScopeMcpServersError, SyncModelScopeMcpServersResponse, ListSkillsData, ListSkillsError, ListSkillsResponse, UploadSkillData, UploadSkillError, UploadSkillResponse, UploadSkillsBatchData, UploadSkillsBatchError, UploadSkillsBatchResponse, UpdateSkillByNameData, UpdateSkillByNameError, UpdateSkillByNameResponse, DeleteSkillByNameData, DeleteSkillByNameError, DeleteSkillByNameResponse, DownloadSkillByNameData, DownloadSkillByNameError, DownloadSkillByNameResponse, ListSkillFilesByNameData, ListSkillFilesByNameError, ListSkillFilesByNameResponse, GetSkillFileByNameData, GetSkillFileByNameError, GetSkillFileByNameResponse, UpdateSkillFileByNameData, UpdateSkillFileByNameError, UpdateSkillFileByNameResponse, UpdateSkillData, UpdateSkillError, UpdateSkillResponse, DeleteSkillData, DeleteSkillError, DeleteSkillResponse, DownloadSkillData, DownloadSkillError, DownloadSkillResponse, ListSkillFilesData, ListSkillFilesError, ListSkillFilesResponse, GetSkillFileData, GetSkillFileError, GetSkillFileResponse, UpdateSkillFileData, UpdateSkillFileError, UpdateSkillFileResponse, ListNeoSkillCandidatesData, ListNeoSkillCandidatesError, ListNeoSkillCandidatesResponse, ListNeoSkillReleasesData, ListNeoSkillReleasesError, ListNeoSkillReleasesResponse, GetNeoSkillPayloadData, GetNeoSkillPayloadError, GetNeoSkillPayloadResponse, EvaluateNeoSkillCandidateData, EvaluateNeoSkillCandidateError, EvaluateNeoSkillCandidateResponse, PromoteNeoSkillCandidateData, PromoteNeoSkillCandidateError, PromoteNeoSkillCandidateResponse, RollbackNeoSkillReleaseData, RollbackNeoSkillReleaseError, RollbackNeoSkillReleaseResponse, SyncNeoSkillReleaseData, SyncNeoSkillReleaseError, SyncNeoSkillReleaseResponse, DeleteNeoSkillCandidateData, DeleteNeoSkillCandidateError, DeleteNeoSkillCandidateResponse, DeleteNeoSkillReleaseData, DeleteNeoSkillReleaseError, DeleteNeoSkillReleaseResponse, ListKnowledgeBasesData, ListKnowledgeBasesError, ListKnowledgeBasesResponse, CreateKnowledgeBaseData, CreateKnowledgeBaseError, CreateKnowledgeBaseResponse, GetKnowledgeBaseData, GetKnowledgeBaseError, GetKnowledgeBaseResponse, UpdateKnowledgeBaseData, UpdateKnowledgeBaseError, UpdateKnowledgeBaseResponse, DeleteKnowledgeBaseData, DeleteKnowledgeBaseError, DeleteKnowledgeBaseResponse, GetKnowledgeBaseStatsData, GetKnowledgeBaseStatsError, GetKnowledgeBaseStatsResponse, ListKnowledgeDocumentsData, ListKnowledgeDocumentsError, ListKnowledgeDocumentsResponse, UploadKnowledgeDocumentData, UploadKnowledgeDocumentError, UploadKnowledgeDocumentResponse, ImportKnowledgeDocumentsData, ImportKnowledgeDocumentsError, ImportKnowledgeDocumentsResponse, ImportKnowledgeDocumentFromUrlData, ImportKnowledgeDocumentFromUrlError, ImportKnowledgeDocumentFromUrlResponse, GetKnowledgeDocumentData, GetKnowledgeDocumentError, GetKnowledgeDocumentResponse, DeleteKnowledgeDocumentData, DeleteKnowledgeDocumentError, DeleteKnowledgeDocumentResponse, ListKnowledgeChunksData, ListKnowledgeChunksError, ListKnowledgeChunksResponse, DeleteKnowledgeChunkData, DeleteKnowledgeChunkError, DeleteKnowledgeChunkResponse, RetrieveKnowledgeBaseData, RetrieveKnowledgeBaseError, RetrieveKnowledgeBaseResponse, GetKnowledgeTaskData, GetKnowledgeTaskError, GetKnowledgeTaskResponse, GetPersonaTreeError, GetPersonaTreeResponse, ListPersonasData, ListPersonasError, ListPersonasResponse, CreatePersonaData, CreatePersonaError, CreatePersonaResponse, GetPersonaByIdData, GetPersonaByIdError, GetPersonaByIdResponse, UpdatePersonaByIdData, UpdatePersonaByIdError, UpdatePersonaByIdResponse, DeletePersonaByIdData, DeletePersonaByIdError, DeletePersonaByIdResponse, GetPersonaData, GetPersonaError, GetPersonaResponse, UpdatePersonaData, UpdatePersonaError, UpdatePersonaResponse, DeletePersonaData, DeletePersonaError, DeletePersonaResponse, ListPersonaFoldersData, ListPersonaFoldersError, ListPersonaFoldersResponse, CreatePersonaFolderData, CreatePersonaFolderError, CreatePersonaFolderResponse, UpdatePersonaFolderData, UpdatePersonaFolderError, UpdatePersonaFolderResponse, DeletePersonaFolderData, DeletePersonaFolderError, DeletePersonaFolderResponse, MovePersonaItemData, MovePersonaItemError, MovePersonaItemResponse, ReorderPersonaItemsData, ReorderPersonaItemsError, ReorderPersonaItemsResponse, ListSessionsData, ListSessionsError, ListSessionsResponse, ListActiveUmosError, ListActiveUmosResponse, ListSessionRulesData, ListSessionRulesError, ListSessionRulesResponse, UpsertSessionRuleData, UpsertSessionRuleError, UpsertSessionRuleResponse, DeleteSessionRulesData, DeleteSessionRulesError, DeleteSessionRulesResponse, BatchUpdateSessionProviderData, BatchUpdateSessionProviderError, BatchUpdateSessionProviderResponse, BatchUpdateSessionServiceData, BatchUpdateSessionServiceError, BatchUpdateSessionServiceResponse, ListSessionGroupsError, ListSessionGroupsResponse, CreateSessionGroupData, CreateSessionGroupError, CreateSessionGroupResponse, UpdateSessionGroupData, UpdateSessionGroupError, UpdateSessionGroupResponse, DeleteSessionGroupData, DeleteSessionGroupError, DeleteSessionGroupResponse, ListConversationsData, ListConversationsError, ListConversationsResponse, BatchDeleteConversationsData, BatchDeleteConversationsError, BatchDeleteConversationsResponse, GetConversationData, GetConversationError, GetConversationResponse, UpdateConversationData, UpdateConversationError, UpdateConversationResponse, DeleteConversationData, DeleteConversationError, DeleteConversationResponse, ReplaceConversationMessagesData, ReplaceConversationMessagesError, ReplaceConversationMessagesResponse, ExportConversationsData, ExportConversationsError, ExportConversationsResponse, GetStatsData, GetStatsError, GetStatsResponse, GetProviderTokenStatsData, GetProviderTokenStatsError, GetProviderTokenStatsResponse, GetVersionError, GetVersionResponse, GetPublicVersionsError, GetPublicVersionsResponse, GetFirstNoticeData, GetFirstNoticeError, GetFirstNoticeResponse, TestGhproxyConnectionData, TestGhproxyConnectionError, TestGhproxyConnectionResponse, ListChangelogVersionsError, ListChangelogVersionsResponse, GetChangelogData, GetChangelogError, GetChangelogResponse, GetStartTimeError, GetStartTimeResponse, GetStorageStatusError, GetStorageStatusResponse, CleanupStorageData, CleanupStorageError, CleanupStorageResponse, RestartCoreError, RestartCoreResponse, ListBackupsData, ListBackupsError, ListBackupsResponse, CreateBackupData, CreateBackupError, CreateBackupResponse, UploadBackupData, UploadBackupError, UploadBackupResponse, InitBackupUploadData, InitBackupUploadError, InitBackupUploadResponse, UploadBackupChunkData, UploadBackupChunkError, UploadBackupChunkResponse, CompleteBackupUploadData, CompleteBackupUploadError, CompleteBackupUploadResponse, AbortBackupUploadData, AbortBackupUploadError, AbortBackupUploadResponse, GetBackupProgressData, GetBackupProgressError, GetBackupProgressResponse, DownloadBackupData, DownloadBackupError, DownloadBackupResponse, RenameBackupData, RenameBackupError, RenameBackupResponse, DeleteBackupData, DeleteBackupError, DeleteBackupResponse, CheckBackupData, CheckBackupError, CheckBackupResponse, ImportBackupData, ImportBackupError, ImportBackupResponse, CheckUpdateError, CheckUpdateResponse, ListReleasesData, ListReleasesError, ListReleasesResponse, UpdateCoreData, UpdateCoreError, UpdateCoreResponse, UpdateDashboardData, UpdateDashboardError, UpdateDashboardResponse, GetUpdateProgressData, GetUpdateProgressError, GetUpdateProgressResponse, InstallPipPackageData, InstallPipPackageError, InstallPipPackageResponse, ListCronJobsData, ListCronJobsError, ListCronJobsResponse, CreateCronJobData, CreateCronJobError, CreateCronJobResponse, UpdateCronJobData, UpdateCronJobError, UpdateCronJobResponse, DeleteCronJobData, DeleteCronJobError, DeleteCronJobResponse, RunCronJobData, RunCronJobError, RunCronJobResponse, StreamLiveLogsError, StreamLiveLogsResponse, GetLogHistoryError, GetLogHistoryResponse, GetTraceSettingsError, GetTraceSettingsResponse, UpdateTraceSettingsData, UpdateTraceSettingsError, UpdateTraceSettingsResponse, ListT2iTemplatesError, ListT2iTemplatesResponse, CreateT2iTemplateData, CreateT2iTemplateError, CreateT2iTemplateResponse, GetActiveT2iTemplateError, GetActiveT2iTemplateResponse, SetActiveT2iTemplateData, SetActiveT2iTemplateError, SetActiveT2iTemplateResponse, ResetDefaultT2iTemplateError, ResetDefaultT2iTemplateResponse, GetT2iTemplateData, GetT2iTemplateError, GetT2iTemplateResponse, UpdateT2iTemplateData, UpdateT2iTemplateError, UpdateT2iTemplateResponse, DeleteT2iTemplateData, DeleteT2iTemplateError, DeleteT2iTemplateResponse, GetSubagentConfigError, GetSubagentConfigResponse, UpdateSubagentConfigData, UpdateSubagentConfigError, UpdateSubagentConfigResponse, ListSubagentAvailableToolsError, ListSubagentAvailableToolsResponse, VerifyPlatformWebhookData, VerifyPlatformWebhookError, VerifyPlatformWebhookResponse, ReceivePlatformWebhookData, ReceivePlatformWebhookError, ReceivePlatformWebhookResponse } from './types.gen'; export const client = createClient(createConfig()); @@ -1290,6 +1290,17 @@ export const updatePluginConfig = (options }); }; +/** + * Set plugin log level + * Set the log level of a plugin. Pass null to follow the global log level. + */ +export const updatePluginLogLevel = (options: OptionsLegacyParser) => { + return (options?.client ?? client).put({ + ...options, + url: '/api/v1/plugins/{plugin_id}/log-level' + }); +}; + /** * Get plugin configuration schema */ diff --git a/dashboard/src/api/generated/openapi-v1/types.gen.ts b/dashboard/src/api/generated/openapi-v1/types.gen.ts index fdb78bb2c6..12ff4dc995 100644 --- a/dashboard/src/api/generated/openapi-v1/types.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/types.gen.ts @@ -1871,6 +1871,23 @@ export type UpdatePluginConfigResponse = (SuccessEnvelope); export type UpdatePluginConfigError = unknown; +export type UpdatePluginLogLevelData = { + body: { + /** + * Log level name, or null to follow the global level. + */ + level?: ('DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL') | null; + [key: string]: unknown | string; + }; + path: { + plugin_id: string; + }; +}; + +export type UpdatePluginLogLevelResponse = (SuccessEnvelope); + +export type UpdatePluginLogLevelError = unknown; + export type GetPluginConfigSchemaData = { path: { plugin_id: string; diff --git a/dashboard/src/api/v1.ts b/dashboard/src/api/v1.ts index 5df8c58561..bc87d155c5 100644 --- a/dashboard/src/api/v1.ts +++ b/dashboard/src/api/v1.ts @@ -1259,6 +1259,17 @@ export const pluginApi = { }), ); }, + updateLogLevel( + pluginId: string, + level: "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL" | null, + ) { + return typed( + openApiV1.updatePluginLogLevel({ + path: { plugin_id: pluginId }, + body: { level }, + }), + ); + }, listConfigFiles(pluginId: string, configKey: string) { return typed( openApiV1.listPluginConfigFilesById({ diff --git a/dashboard/src/i18n/locales/en-US/features/extension.json b/dashboard/src/i18n/locales/en-US/features/extension.json index 75313fdba9..60049866df 100644 --- a/dashboard/src/i18n/locales/en-US/features/extension.json +++ b/dashboard/src/i18n/locales/en-US/features/extension.json @@ -196,7 +196,12 @@ }, "config": { "title": "Extension Configuration", - "noConfig": "This extension has no configuration" + "noConfig": "This extension has no additional configuration", + "coreSettings": { + "logLevel": "Log Level", + "logLevelHint": "Only affects this plugin's log output. Takes effect immediately, no restart required.", + "followGlobal": "Follow Global" + } }, "loading": { "title": "Loading...", @@ -264,6 +269,7 @@ "refreshSuccess": "Extension list refreshed!", "refreshFailed": "Error occurred while refreshing extension list", "operationFailed": "Operation failed", + "logLevelUpdated": "Log level updated and applied immediately", "reloadSuccess": "Reload successful", "reloadFailed": "Reload failed", "updateSuccess": "Update successful!", diff --git a/dashboard/src/i18n/locales/ru-RU/features/extension.json b/dashboard/src/i18n/locales/ru-RU/features/extension.json index d876f08efd..5af6953bca 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/extension.json +++ b/dashboard/src/i18n/locales/ru-RU/features/extension.json @@ -195,7 +195,12 @@ }, "config": { "title": "Настройка плагина", - "noConfig": "У этого плагина нет настраиваемых параметров" + "noConfig": "У этого плагина нет других настраиваемых параметров", + "coreSettings": { + "logLevel": "Уровень логирования", + "logLevelHint": "Влияет только на логи этого плагина. Применяется сразу, перезапуск не требуется.", + "followGlobal": "Как глобально" + } }, "loading": { "title": "Загрузка...", @@ -263,6 +268,7 @@ "refreshSuccess": "Список плагинов обновлен", "refreshFailed": "Ошибка при обновлении списка", "operationFailed": "Ошибка операции", + "logLevelUpdated": "Уровень логирования обновлён и применён сразу", "reloadSuccess": "Перезагрузка завершена", "reloadFailed": "Ошибка перезагрузки", "updateSuccess": "Обновление завершено", diff --git a/dashboard/src/i18n/locales/zh-CN/features/extension.json b/dashboard/src/i18n/locales/zh-CN/features/extension.json index d8ba9869e6..8b0468a6ac 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/extension.json +++ b/dashboard/src/i18n/locales/zh-CN/features/extension.json @@ -196,7 +196,12 @@ }, "config": { "title": "插件配置", - "noConfig": "这个插件没有配置" + "noConfig": "这个插件没有其他配置项", + "coreSettings": { + "logLevel": "日志级别", + "logLevelHint": "仅影响该插件的日志输出,立即生效,无需重启", + "followGlobal": "跟随全局" + } }, "loading": { "title": "加载中...", @@ -264,6 +269,7 @@ "refreshSuccess": "插件列表已刷新!", "refreshFailed": "刷新插件列表时发生错误", "operationFailed": "操作失败", + "logLevelUpdated": "日志级别已更新并实时生效", "reloadSuccess": "重载成功", "reloadFailed": "重载失败", "updateSuccess": "更新成功!", diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index df88f3df0f..132e408b96 100644 --- a/dashboard/src/views/ExtensionPage.vue +++ b/dashboard/src/views/ExtensionPage.vue @@ -128,6 +128,8 @@ const { pluginOff, openExtensionConfig, updateConfig, + updatePluginLogLevel, + pluginLogLevelSaving, showPluginInfo, reloadPlugin, viewReadme, @@ -171,6 +173,15 @@ const { searchDebounceTimer, } = pageState; +const logLevelItems = computed(() => [ + { title: tm("dialogs.config.coreSettings.followGlobal"), value: null }, + { title: "DEBUG", value: "DEBUG" }, + { title: "INFO", value: "INFO" }, + { title: "WARNING", value: "WARNING" }, + { title: "ERROR", value: "ERROR" }, + { title: "CRITICAL", value: "CRITICAL" }, +]); + const selectedPluginId = computed(() => { const pluginId = route.params.pluginId; return Array.isArray(pluginId) ? pluginId[0] : pluginId || ""; @@ -431,6 +442,30 @@ const updateDialogPluginLogo = computed(() => { tm("dialogs.config.title") }} +
+
+
+ {{ tm("dialogs.config.coreSettings.logLevel") }} +
+
+ {{ tm("dialogs.config.coreSettings.logLevelHint") }} +
+
+ +
+
{ - {{ - tm("buttons.saveAndClose") - }} + {{ tm("buttons.saveAndClose") }} { metadata: {}, config: {}, i18n: {}, + log_level: null, }); + const pluginLogLevelSaving = ref(false); const pluginMarketData = ref([]); const loadingDialog = reactive({ show: false, @@ -1084,16 +1086,49 @@ export const useExtensionPage = () => { curr_namespace.value = extension_name; currentConfigPlugin.value = extension_name; configDialog.value = true; + extension_config.log_level = null; try { const res = await pluginApi.config(extension_name); + // Discard the response if the user has already switched to another + // plugin's config dialog while the request was in flight. + if (curr_namespace.value !== extension_name) return; extension_config.metadata = res.data.data.metadata; extension_config.config = res.data.data.config; extension_config.i18n = res.data.data.i18n || {}; + extension_config.log_level = res.data.data.log_level ?? null; } catch (err) { toast(err, "error"); } }; + const updatePluginLogLevel = async (level) => { + if (pluginLogLevelSaving.value) return; + const pluginName = curr_namespace.value; + const previous = extension_config.log_level; + extension_config.log_level = level; + pluginLogLevelSaving.value = true; + try { + const res = await pluginApi.updateLogLevel(pluginName, level); + if (res.data.status === "ok") { + toast(tm("messages.logLevelUpdated"), "success"); + } else { + // Roll back the optimistic update, unless the dialog has already + // been switched to another plugin. + if (curr_namespace.value === pluginName) { + extension_config.log_level = previous; + } + toast(res.data.message || tm("messages.operationFailed"), "error"); + } + } catch (err) { + if (curr_namespace.value === pluginName) { + extension_config.log_level = previous; + } + toast(err, "error"); + } finally { + pluginLogLevelSaving.value = false; + } + }; + const updateConfig = async () => { try { const res = await pluginApi.updateConfig( @@ -1110,6 +1145,7 @@ export const useExtensionPage = () => { extension_config.metadata = {}; extension_config.config = {}; extension_config.i18n = {}; + extension_config.log_level = null; getExtensions(); } catch (err) { toast(err, "error"); @@ -2465,6 +2501,8 @@ export const useExtensionPage = () => { pluginOff, openExtensionConfig, updateConfig, + updatePluginLogLevel, + pluginLogLevelSaving, showPluginInfo, reloadPlugin, viewReadme, diff --git a/openspec/openapi-v1.yaml b/openspec/openapi-v1.yaml index 37319f8c82..98d93d401b 100644 --- a/openspec/openapi-v1.yaml +++ b/openspec/openapi-v1.yaml @@ -2075,6 +2075,32 @@ paths: "200": $ref: "#/components/responses/Ok" + /api/v1/plugins/{plugin_id}/log-level: + put: + tags: [Plugins] + summary: Set plugin log level + description: Set the log level of a plugin. Pass null to follow the global log level. + operationId: updatePluginLogLevel + x-astrbot-scope: plugin + parameters: + - $ref: "#/components/parameters/PluginId" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + level: + type: string + nullable: true + enum: [DEBUG, INFO, WARNING, ERROR, CRITICAL, null] + description: Log level name, or null to follow the global level. + additionalProperties: true + responses: + "200": + $ref: "#/components/responses/Ok" + /api/v1/plugins/{plugin_id}/config/schema: get: tags: [Plugins] diff --git a/tests/test_fastapi_v1_dashboard.py b/tests/test_fastapi_v1_dashboard.py index f129c1e8de..3e03a4cef8 100644 --- a/tests/test_fastapi_v1_dashboard.py +++ b/tests/test_fastapi_v1_dashboard.py @@ -2843,6 +2843,11 @@ def fake_list_config_files(*, scope: str, name: str, key_path: str): params={"plugin_id": plugin_id}, headers=headers, ) + config_response = await asgi_client.get( + "/api/v1/plugins/config", + params={"plugin_id": plugin_id}, + headers=headers, + ) config_files_response = await asgi_client.get( "/api/v1/plugins/config-files", params={"plugin_id": plugin_id, "config_key": "assets/path"}, @@ -2863,6 +2868,12 @@ def fake_list_config_files(*, scope: str, name: str, key_path: str): } assert readme_response.status_code == 200 assert readme_response.json()["data"]["name"] == plugin_id + assert config_response.status_code == 200 + assert config_response.json()["data"] == { + "plugin_name": plugin_id, + "log_level": None, + "schema": {"name": plugin_id}, + } assert schema_response.status_code == 200 assert schema_response.json()["data"]["plugin_name"] == plugin_id assert config_files_response.status_code == 200 From 70724115ece3a0d8b6352274f642fcee0032c52b Mon Sep 17 00:00:00 2001 From: RuochenPan Date: Tue, 21 Jul 2026 17:25:20 +0800 Subject: [PATCH 3/7] fix(logging): support legacy plugin loggers --- astrbot/api/__init__.py | 3 +- tests/unit/test_api_logger.py | 54 +++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/astrbot/api/__init__.py b/astrbot/api/__init__.py index 5c2d4598f8..3c6d1e6a10 100644 --- a/astrbot/api/__init__.py +++ b/astrbot/api/__init__.py @@ -30,6 +30,7 @@ def _resolve_caller_logger(module_name: str) -> logging.Logger: the caller does not belong to a registered plugin. """ # Imported lazily to avoid a circular import with astrbot.core.star. + from astrbot.core.log import LogManager from astrbot.core.star.star import star_map cached = _logger_cache.get(module_name) @@ -47,7 +48,7 @@ def _resolve_caller_logger(module_name: str) -> logging.Logger: continue package = module_path.rpartition(".")[0] if module_name == module_path or module_name.startswith(package + "."): - resolved = logging.getLogger(f"astrbot.plugin.{metadata.name}") + resolved = LogManager.get_plugin_logger(metadata.name) _logger_cache[module_name] = (module_path, metadata.name, resolved) return resolved diff --git a/tests/unit/test_api_logger.py b/tests/unit/test_api_logger.py index 2b3d8e9694..ec6c7a4e94 100644 --- a/tests/unit/test_api_logger.py +++ b/tests/unit/test_api_logger.py @@ -7,6 +7,7 @@ import astrbot.api as api from astrbot.core.log import LogManager +from astrbot.core.star import Star from astrbot.core.star.star import StarMetadata, star_map @@ -20,6 +21,8 @@ def test_plugin_logger_cache_refreshes_after_plugin_rename( """ module_path = "data.plugins.cache_refresh.main" caller_module = "data.plugins.cache_refresh.helpers" + monkeypatch.setattr(LogManager, "_plugin_logger_names", set()) + monkeypatch.setattr(LogManager, "_plugin_level_overrides", {}) monkeypatch.setitem( star_map, module_path, @@ -63,3 +66,54 @@ def test_global_level_sync_handles_mock_logger( assert plugin_logger.level == logging.WARNING finally: plugin_logger.setLevel(previous_level) + + +def test_legacy_plugin_without_super_uses_dedicated_logger( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ensure the historical API logger works without ``Star.__init__``. + + Args: + monkeypatch: Pytest fixture used to isolate logger and registry state. + """ + module_path = "data.plugins.legacy_logger.main" + namespace = { + "__name__": module_path, + "Star": Star, + "logger": api.logger, + } + exec( + "class LegacyPlugin(Star):\n" + " def __init__(self, context):\n" + " self.context = context\n" + " def logger_state(self):\n" + " return (\n" + " logger.name,\n" + " logger.getEffectiveLevel(),\n" + " logger.isEnabledFor(20),\n" + " logger.isEnabledFor(30),\n" + " )\n", + namespace, + ) + star_map[module_path].name = "legacy_logger" + monkeypatch.setattr(LogManager, "_plugin_logger_names", set()) + monkeypatch.setattr(LogManager, "_log_broker", None) + monkeypatch.setattr( + LogManager, + "_plugin_level_overrides", + {"legacy_logger": "WARNING"}, + ) + api._logger_cache.pop(module_path, None) + + try: + plugin = namespace["LegacyPlugin"](object()) + + assert plugin.logger_state() == ( + "astrbot.plugin.legacy_logger", + logging.WARNING, + False, + True, + ) + assert "legacy_logger" in LogManager._plugin_logger_names + finally: + api._logger_cache.pop(module_path, None) From 01802af399c9bec1771b9ac5b62b5eb1deab008b Mon Sep 17 00:00:00 2001 From: RuochenPan Date: Tue, 21 Jul 2026 17:36:33 +0800 Subject: [PATCH 4/7] fix(dashboard): sync plugin log level response --- dashboard/src/views/extension/useExtensionPage.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dashboard/src/views/extension/useExtensionPage.js b/dashboard/src/views/extension/useExtensionPage.js index 8c12e1424a..0693ce01df 100644 --- a/dashboard/src/views/extension/useExtensionPage.js +++ b/dashboard/src/views/extension/useExtensionPage.js @@ -1110,6 +1110,15 @@ export const useExtensionPage = () => { try { const res = await pluginApi.updateLogLevel(pluginName, level); if (res.data.status === "ok") { + const serverLevel = res.data.data?.log_level; + // Preserve null as "follow global" and only skip synchronization + // when an older server does not return the field. + if ( + curr_namespace.value === pluginName && + serverLevel !== undefined + ) { + extension_config.log_level = serverLevel; + } toast(tm("messages.logLevelUpdated"), "success"); } else { // Roll back the optimistic update, unless the dialog has already From 48a04885301ce4b8aa79ebc380440496b43d233b Mon Sep 17 00:00:00 2001 From: RuochenPan Date: Tue, 21 Jul 2026 17:40:57 +0800 Subject: [PATCH 5/7] perf(logging): avoid rescanning plugin handlers --- astrbot/core/log.py | 9 +++---- tests/unit/test_api_logger.py | 45 ++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/astrbot/core/log.py b/astrbot/core/log.py index 21dcedf58a..71094c97f7 100644 --- a/astrbot/core/log.py +++ b/astrbot/core/log.py @@ -380,10 +380,11 @@ def set_queue_handler(cls, logger: logging.Logger, log_broker: LogBroker) -> Non cls._log_broker = log_broker targets = [logger] - targets.extend( - logging.getLogger(f"{PLUGIN_LOGGER_PREFIX}{name}") - for name in cls._plugin_logger_names - ) + if logger.name == "astrbot": + targets.extend( + logging.getLogger(f"{PLUGIN_LOGGER_PREFIX}{name}") + for name in cls._plugin_logger_names + ) for target in targets: cls._ensure_logger_enricher_filter(target) diff --git a/tests/unit/test_api_logger.py b/tests/unit/test_api_logger.py index ec6c7a4e94..0499e68c55 100644 --- a/tests/unit/test_api_logger.py +++ b/tests/unit/test_api_logger.py @@ -6,7 +6,7 @@ import pytest import astrbot.api as api -from astrbot.core.log import LogManager +from astrbot.core.log import LogBroker, LogManager, LogQueueHandler from astrbot.core.star import Star from astrbot.core.star.star import StarMetadata, star_map @@ -117,3 +117,46 @@ def test_legacy_plugin_without_super_uses_dedicated_logger( assert "legacy_logger" in LogManager._plugin_logger_names finally: api._logger_cache.pop(module_path, None) + + +def test_queue_handler_only_scans_plugins_for_global_logger( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ensure registering a plugin logger does not rescan every plugin. + + Args: + monkeypatch: Pytest fixture used to isolate LogManager class state. + """ + broker = LogBroker() + existing_name = "queue_handler_existing" + existing_logger = logging.getLogger(f"astrbot.plugin.{existing_name}") + previous_handlers = existing_logger.handlers.copy() + previous_filters = existing_logger.filters.copy() + existing_logger.handlers.clear() + existing_logger.filters.clear() + plugin_logger = logging.Logger("astrbot.plugin.queue_handler_new") + global_logger = logging.Logger("astrbot") + monkeypatch.setattr(LogManager, "_plugin_logger_names", {existing_name}) + monkeypatch.setattr(LogManager, "_log_broker", None) + + try: + LogManager.set_queue_handler(plugin_logger, broker) + + assert any( + isinstance(handler, LogQueueHandler) for handler in plugin_logger.handlers + ) + assert not any( + isinstance(handler, LogQueueHandler) for handler in existing_logger.handlers + ) + + LogManager.set_queue_handler(global_logger, broker) + + assert any( + isinstance(handler, LogQueueHandler) for handler in global_logger.handlers + ) + assert any( + isinstance(handler, LogQueueHandler) for handler in existing_logger.handlers + ) + finally: + existing_logger.handlers[:] = previous_handlers + existing_logger.filters[:] = previous_filters From 9acf1325f3caf8dd9cd323c2ae308ffa5fb56d95 Mon Sep 17 00:00:00 2001 From: RuochenPan Date: Tue, 21 Jul 2026 17:42:11 +0800 Subject: [PATCH 6/7] fix(logging): persist plugin levels atomically --- astrbot/core/log.py | 36 +++++++++++++++---- tests/unit/test_api_logger.py | 68 ++++++++++++++++++++++++++++++++++- 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/astrbot/core/log.py b/astrbot/core/log.py index 71094c97f7..9ec1a30b05 100644 --- a/astrbot/core/log.py +++ b/astrbot/core/log.py @@ -5,9 +5,11 @@ import logging import os import sys +import tempfile import time from asyncio import Queue from collections import deque +from pathlib import Path from typing import TYPE_CHECKING from loguru import logger as _raw_loguru_logger @@ -282,8 +284,8 @@ def GetLogger(cls, log_name: str = "default") -> logging.Logger: return logger @classmethod - def _plugin_log_levels_path(cls) -> str: - return os.path.join(get_astrbot_config_path(), "plugin_log_levels.json") + def _plugin_log_levels_path(cls) -> Path: + return Path(get_astrbot_config_path()) / "plugin_log_levels.json" @classmethod def _load_plugin_level_overrides(cls) -> dict[str, str]: @@ -291,7 +293,7 @@ def _load_plugin_level_overrides(cls) -> dict[str, str]: if cls._plugin_level_overrides is None: cls._plugin_level_overrides = {} try: - with open(cls._plugin_log_levels_path(), encoding="utf-8") as f: + with cls._plugin_log_levels_path().open(encoding="utf-8") as f: data = json.load(f) if isinstance(data, dict): cls._plugin_level_overrides = { @@ -326,7 +328,7 @@ def set_plugin_log_level(cls, plugin_name: str, level: str | None) -> None: Raises: ValueError: If the level name is not valid. """ - overrides = cls._load_plugin_level_overrides() + overrides = dict(cls._load_plugin_level_overrides()) if level is None: overrides.pop(plugin_name, None) else: @@ -335,9 +337,29 @@ def set_plugin_log_level(cls, plugin_name: str, level: str | None) -> None: raise ValueError(f"Invalid log level: {level}") overrides[plugin_name] = level - os.makedirs(os.path.dirname(cls._plugin_log_levels_path()), exist_ok=True) - with open(cls._plugin_log_levels_path(), "w", encoding="utf-8") as f: - json.dump(overrides, f, indent=2) + config_path = cls._plugin_log_levels_path() + config_path.parent.mkdir(parents=True, exist_ok=True) + temp_path: Path | None = None + try: + with tempfile.NamedTemporaryFile( + "w", + encoding="utf-8", + dir=config_path.parent, + prefix=f".{config_path.name}.", + suffix=".tmp", + delete=False, + ) as f: + temp_path = Path(f.name) + json.dump(overrides, f, indent=2) + f.flush() + os.fsync(f.fileno()) + temp_path.replace(config_path) + except Exception: + if temp_path is not None: + temp_path.unlink(missing_ok=True) + raise + + cls._plugin_level_overrides = overrides if plugin_name in cls._plugin_logger_names: logging.getLogger(f"{PLUGIN_LOGGER_PREFIX}{plugin_name}").setLevel( diff --git a/tests/unit/test_api_logger.py b/tests/unit/test_api_logger.py index 0499e68c55..48568e191f 100644 --- a/tests/unit/test_api_logger.py +++ b/tests/unit/test_api_logger.py @@ -1,7 +1,9 @@ """Tests for the plugin-aware logger exposed by ``astrbot.api``.""" +import json import logging -from unittest.mock import MagicMock +from pathlib import Path +from unittest.mock import MagicMock, patch import pytest @@ -160,3 +162,67 @@ def test_queue_handler_only_scans_plugins_for_global_logger( finally: existing_logger.handlers[:] = previous_handlers existing_logger.filters[:] = previous_filters + + +def test_plugin_log_level_is_persisted_atomically( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ensure a plugin log-level update atomically replaces its config file. + + Args: + tmp_path: Temporary directory for the persisted configuration. + monkeypatch: Pytest fixture used to isolate LogManager class state. + """ + config_path = tmp_path / "plugin_log_levels.json" + monkeypatch.setattr( + LogManager, + "_plugin_log_levels_path", + MagicMock(return_value=config_path), + ) + monkeypatch.setattr(LogManager, "_plugin_level_overrides", {}) + monkeypatch.setattr(LogManager, "_plugin_logger_names", set()) + + LogManager.set_plugin_log_level("atomic_plugin", "warning") + + assert json.loads(config_path.read_text(encoding="utf-8")) == { + "atomic_plugin": "WARNING" + } + assert LogManager._plugin_level_overrides == {"atomic_plugin": "WARNING"} + assert not list(tmp_path.glob("*.tmp")) + + +def test_plugin_log_level_write_failure_preserves_previous_state( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Ensure a failed atomic replace preserves disk and memory state. + + Args: + tmp_path: Temporary directory for the persisted configuration. + monkeypatch: Pytest fixture used to isolate LogManager class state. + """ + config_path = tmp_path / "plugin_log_levels.json" + config_path.write_text('{"existing_plugin": "INFO"}', encoding="utf-8") + previous_overrides = {"existing_plugin": "INFO"} + monkeypatch.setattr( + LogManager, + "_plugin_log_levels_path", + MagicMock(return_value=config_path), + ) + monkeypatch.setattr( + LogManager, + "_plugin_level_overrides", + previous_overrides, + ) + monkeypatch.setattr(LogManager, "_plugin_logger_names", set()) + + with ( + patch.object(Path, "replace", side_effect=OSError("replace failed")), + pytest.raises(OSError, match="replace failed"), + ): + LogManager.set_plugin_log_level("new_plugin", "ERROR") + + assert json.loads(config_path.read_text(encoding="utf-8")) == previous_overrides + assert LogManager._plugin_level_overrides is previous_overrides + assert not list(tmp_path.glob("*.tmp")) From 49f8d56635b364b953ecc1e016f0fbab14aa03d5 Mon Sep 17 00:00:00 2001 From: RuochenPan Date: Tue, 21 Jul 2026 17:43:20 +0800 Subject: [PATCH 7/7] test(logging): use valid logger level mocks --- astrbot/core/log.py | 7 ------- tests/unit/test_api_logger.py | 8 ++++---- tests/unit/test_core_lifecycle.py | 2 ++ 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/astrbot/core/log.py b/astrbot/core/log.py index 9ec1a30b05..b827d9d704 100644 --- a/astrbot/core/log.py +++ b/astrbot/core/log.py @@ -498,13 +498,6 @@ def configure_logger( # Plugin loggers without an explicit override follow the global level. plugin_level = logger.level - if not isinstance(plugin_level, int): - configured_level = logging.getLevelName(str(level).upper()) - plugin_level = ( - configured_level - if isinstance(configured_level, int) - else logging.INFO - ) overrides = cls._load_plugin_level_overrides() for name in cls._plugin_logger_names: if name not in overrides: diff --git a/tests/unit/test_api_logger.py b/tests/unit/test_api_logger.py index 48568e191f..d95a3cec78 100644 --- a/tests/unit/test_api_logger.py +++ b/tests/unit/test_api_logger.py @@ -46,10 +46,10 @@ def test_plugin_logger_cache_refreshes_after_plugin_rename( api._logger_cache.pop(caller_module, None) -def test_global_level_sync_handles_mock_logger( +def test_global_level_sync_updates_plugin_loggers( monkeypatch: pytest.MonkeyPatch, ) -> None: - """Ensure global level syncing always passes a valid logging level. + """Ensure global level syncing updates plugins without overrides. Args: monkeypatch: Pytest fixture used to isolate LogManager class state. @@ -57,14 +57,14 @@ def test_global_level_sync_handles_mock_logger( plugin_name = "mock_level_sync" plugin_logger = logging.getLogger(f"astrbot.plugin.{plugin_name}") previous_level = plugin_logger.level - global_logger = MagicMock() - global_logger.level = MagicMock() + global_logger = logging.Logger("global_level_sync") monkeypatch.setattr(LogManager, "_plugin_logger_names", {plugin_name}) monkeypatch.setattr(LogManager, "_plugin_level_overrides", {}) try: LogManager.configure_logger(global_logger, {"log_level": "WARNING"}) + assert global_logger.level == logging.WARNING assert plugin_logger.level == logging.WARNING finally: plugin_logger.setLevel(previous_level) diff --git a/tests/unit/test_core_lifecycle.py b/tests/unit/test_core_lifecycle.py index 1fc8035e48..c0549651be 100644 --- a/tests/unit/test_core_lifecycle.py +++ b/tests/unit/test_core_lifecycle.py @@ -1,6 +1,7 @@ """Tests for AstrBotCoreLifecycle.""" import asyncio +import logging import os from unittest.mock import AsyncMock, MagicMock, patch @@ -609,6 +610,7 @@ async def test_initialize_handles_migration_failure( new_callable=AsyncMock, ), ): + mock_logger.level = logging.INFO # Should not raise, just log the error await lifecycle.initialize()