diff --git a/astrbot/core/astrbot_config_mgr.py b/astrbot/core/astrbot_config_mgr.py index c2bfb1c37b..458407e2c6 100644 --- a/astrbot/core/astrbot_config_mgr.py +++ b/astrbot/core/astrbot_config_mgr.py @@ -1,10 +1,22 @@ import os import uuid +from contextvars import Token from typing import TypedDict, TypeVar from astrbot.core import AstrBotConfig, logger from astrbot.core.config.astrbot_config import ASTRBOT_CONFIG_PATH from astrbot.core.config.default import DEFAULT_CONFIG +from astrbot.core.config.overrides import ( + CORE_CONFIG_OVERRIDE_KEY, + build_effective_core_config, + build_effective_core_config_sync, + get_current_effective_config, + normalize_config_override_payload, + remove_core_config_override_paths, + reset_current_effective_config, + set_current_effective_config, + update_core_config_override_paths, +) from astrbot.core.platform.message_session import MessageSession from astrbot.core.umop_config_router import UmopConfigRouter from astrbot.core.utils.astrbot_path import get_astrbot_config_path @@ -31,6 +43,8 @@ class ConfInfo(TypedDict): class AstrBotConfigManager: """A class to manage the system configuration of AstrBot, aka ACM""" + core_config_override_key = CORE_CONFIG_OVERRIDE_KEY + def __init__( self, default_config: AstrBotConfig, @@ -121,8 +135,15 @@ def _save_conf_mapping( self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global") self.abconf_data = abconf_data - def get_conf(self, umo: str | MessageSession | None) -> AstrBotConfig: - """获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。""" + def get_base_conf(self, umo: str | MessageSession | None) -> AstrBotConfig: + """Get the base config profile for a UMO without applying overrides. + + Args: + umo: Unified message origin or message session. + + Returns: + Shared base config profile selected for the UMO. + """ if not umo: return self.confs["default"] if isinstance(umo, MessageSession): @@ -136,6 +157,116 @@ def get_conf(self, umo: str | MessageSession | None) -> AstrBotConfig: return conf + def get_conf(self, umo: str | MessageSession | None = None) -> AstrBotConfig: + """获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。""" + umo_str = None + if umo: + umo_str = ( + f"{umo.platform_id}:{umo.message_type}:{umo.session_id}" + if isinstance(umo, MessageSession) + else str(umo) + ) + + effective_config = get_current_effective_config(umo_str) + if effective_config: + return effective_config + + if not umo: + effective_config = get_current_effective_config() + if effective_config: + return effective_config + return self.confs["default"] + + base_config = self.get_base_conf(umo) + return build_effective_core_config_sync( + base_config, self.sp, umo_str or str(umo) + ) + + def get_current_conf(self, default: AstrBotConfig | None = None) -> AstrBotConfig: + """Get the active task config or a caller-provided default config. + + Args: + default: Config returned when no event-scoped config is active. + + Returns: + Active effective config, default config, or the global default profile. + """ + return get_current_effective_config() or default or self.confs["default"] + + async def build_effective_conf( + self, + umo: str, + base_config: AstrBotConfig | None = None, + ) -> AstrBotConfig: + """Build an effective config for one UMO. + + Args: + umo: Unified message origin. + base_config: Optional already-selected base config profile. + + Returns: + Config view with UMO overrides applied. + """ + return await build_effective_core_config( + base_config or self.get_base_conf(umo), + self.sp, + umo, + ) + + def activate_effective_conf( + self, + umo: str, + config: AstrBotConfig, + ) -> Token: + """Bind an effective config to the current async task. + + Args: + umo: Unified message origin. + config: Effective config for the current event. + + Returns: + Context variable token used to reset the binding. + """ + return set_current_effective_config(umo, config) + + def reset_effective_conf(self, token: Token) -> None: + """Reset the current task's effective config binding. + + Args: + token: Token returned by activate_effective_conf. + """ + reset_current_effective_config(token) + + async def update_conf_overrides(self, umo: str, paths: dict) -> None: + """Merge config override paths for one UMO. + + Args: + umo: Unified message origin. + paths: Dot-separated config path to override value mapping. + """ + await update_core_config_override_paths(self.sp, umo, paths) + + @staticmethod + def normalize_conf_override_payload(payload: object) -> dict: + """Normalize a config override payload into path-value pairs. + + Args: + payload: Raw preference value for a config override. + + Returns: + Dot-separated config path to override value mapping. + """ + return normalize_config_override_payload(payload) + + async def remove_conf_overrides(self, umo: str, paths: list[str]) -> None: + """Remove config override paths for one UMO. + + Args: + umo: Unified message origin. + paths: Dot-separated config paths to remove. + """ + await remove_core_config_override_paths(self.sp, umo, paths) + @property def default_conf(self) -> AstrBotConfig: """获取默认配置文件""" diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 6156601248..5897e6f1d0 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -66,6 +66,7 @@ "forward_threshold": 1500, "enable_id_white_list": True, "id_whitelist": [], + "id_blacklist": [], "id_whitelist_log": True, "wl_ignore_admin_on_group": True, "wl_ignore_admin_on_friend": True, @@ -306,6 +307,7 @@ "callback_api_base": "", "default_kb_collection": "", # 默认知识库名称, 已经过时 "plugin_set": ["*"], # "*" 表示使用所有可用的插件, 空列表表示不使用任何插件 + "plugin_disabled_set": [], "kb_names": [], # 默认知识库名称列表 "kb_fusion_top_k": 20, # 知识库检索融合阶段返回结果数量 "kb_final_top_k": 5, # 知识库检索最终返回结果数量 @@ -1087,6 +1089,11 @@ "items": {"type": "string"}, "hint": "只处理填写的 ID 发来的消息事件,为空时不启用。可使用 /sid 指令获取在平台上的会话 ID(类似 abc:GroupMessage:123)。管理员可在 WebUI 的平台设置中管理白名单", }, + "id_blacklist": { + "type": "list", + "items": {"type": "string"}, + "hint": "拒绝处理填写的 ID 发来的消息事件。命中黑名单时会优先于白名单被拒绝。", + }, "id_whitelist_log": { "type": "bool", "hint": "启用后,当一条消息没通过白名单时,会输出 INFO 级别的日志。", @@ -3924,6 +3931,12 @@ "items": {"type": "string"}, "hint": "使用 /sid 获取 ID。当白名单列表为空时,代表不启用白名单(即所有 ID 都在白名单内)。", }, + "platform_settings.id_blacklist": { + "description": "黑名单 ID 列表", + "type": "list", + "items": {"type": "string"}, + "hint": "使用 /sid 获取 ID。命中黑名单的会话会被拒绝,黑名单优先于白名单。", + }, "platform_settings.id_whitelist_log": { "description": "输出日志", "type": "bool", @@ -4090,6 +4103,12 @@ "hint": "默认启用全部未被禁用的插件。若插件在插件页面被禁用,则此处的选择不会生效。", "_special": "select_plugin_set", }, + "plugin_disabled_set": { + "description": "禁用插件", + "type": "bool", + "hint": "明确禁用的插件列表。命中禁用列表的插件不会在会话中触发。", + "_special": "select_plugin_set", + }, }, }, }, diff --git a/astrbot/core/config/overrides.py b/astrbot/core/config/overrides.py new file mode 100644 index 0000000000..06b314853d --- /dev/null +++ b/astrbot/core/config/overrides.py @@ -0,0 +1,316 @@ +from __future__ import annotations + +import copy +from contextvars import ContextVar, Token +from typing import Any, NamedTuple + +from astrbot.core.config.astrbot_config import AstrBotConfig +from astrbot.core.utils.shared_preferences import SharedPreferences + +CORE_CONFIG_OVERRIDE_KEY = "config_override:core" +PLUGIN_CONFIG_OVERRIDE_PREFIX = "config_override:plugin:" +CONFIG_OVERRIDE_VERSION = 1 + + +class EffectiveConfigContext(NamedTuple): + umo: str + config: AstrBotConfig + + +_current_effective_config: ContextVar[EffectiveConfigContext | None] = ContextVar( + "astrbot_current_effective_config", + default=None, +) + + +class EffectiveAstrBotConfig(dict): + """Per-event AstrBot config view with no persistent file writes. + + Args: + data: Effective config data after applying override paths. + base_config: Config instance used as the immutable base for this view. + umo: Unified message origin this view belongs to. + """ + + def __init__( + self, + data: dict[str, Any], + *, + base_config: AstrBotConfig, + umo: str, + ) -> None: + super().__init__(data) + object.__setattr__(self, "_base_config", base_config) + object.__setattr__(self, "_umo", umo) + object.__setattr__(self, "config_path", base_config.config_path) + object.__setattr__(self, "default_config", base_config.default_config) + object.__setattr__(self, "schema", base_config.schema) + + def save_config( + self, + replace_config: dict | None = None, + *, + indent: int = 2, + ) -> None: + """Keep save-compatible call sites from writing an override view to disk. + + Args: + replace_config: Optional replacement data for the in-memory view. + indent: Preserved for API compatibility with AstrBotConfig. + """ + _ = indent + if replace_config: + self.clear() + self.update(copy.deepcopy(replace_config)) + + def __getattr__(self, item: str) -> Any: + try: + return self[item] + except KeyError: + return None + + def __setattr__(self, key: str, value: Any) -> None: + self[key] = value + + +def get_current_effective_config(umo: str | None = None) -> AstrBotConfig | None: + """Return the current task's effective config, if it matches the UMO. + + Args: + umo: Optional unified message origin to match. + + Returns: + The current effective config, or None when outside an event override scope. + """ + current = _current_effective_config.get() + if current is None: + return None + if umo is not None and current.umo != umo: + return None + return current.config + + +def set_current_effective_config( + umo: str, + config: AstrBotConfig, +) -> Token[EffectiveConfigContext | None]: + """Bind an effective config to the current async task. + + Args: + umo: Unified message origin the config belongs to. + config: Effective config for the current event. + + Returns: + Context variable token used to reset the binding. + """ + return _current_effective_config.set(EffectiveConfigContext(umo, config)) + + +def reset_current_effective_config(token: Token[EffectiveConfigContext | None]) -> None: + """Reset the current task's effective config binding. + + Args: + token: Token returned by set_current_effective_config. + """ + _current_effective_config.reset(token) + + +def normalize_config_override_payload(payload: Any) -> dict[str, Any]: + """Normalize a stored override payload into path-value pairs. + + Args: + payload: Raw preference value for a config override. + + Returns: + A mapping from dot-separated config path to override value. + """ + if not isinstance(payload, dict): + return {} + paths = payload.get("paths", payload) + if not isinstance(paths, dict): + return {} + return { + path: copy.deepcopy(value) + for path, value in paths.items() + if isinstance(path, str) and path.strip() + } + + +async def load_core_config_override_paths( + preferences: SharedPreferences, + umo: str, +) -> dict[str, Any]: + """Load core config override paths for a UMO. + + Args: + preferences: Shared preferences storage. + umo: Unified message origin. + + Returns: + Path-value override mapping from the stored core override payload. + """ + override_payload = await preferences.session_get( + umo, + CORE_CONFIG_OVERRIDE_KEY, + default={}, + ) + return normalize_config_override_payload(override_payload) + + +def load_core_config_override_paths_sync( + preferences: SharedPreferences, + umo: str, +) -> dict[str, Any]: + """Synchronously load core config override paths for a UMO. + + Args: + preferences: Shared preferences storage. + umo: Unified message origin. + + Returns: + Path-value override mapping from the stored core override payload. + """ + override_payload = preferences.get( + CORE_CONFIG_OVERRIDE_KEY, + default={}, + scope="umo", + scope_id=umo, + ) + return normalize_config_override_payload(override_payload) + + +async def save_core_config_override_paths( + preferences: SharedPreferences, + umo: str, + paths: dict[str, Any], +) -> None: + """Persist core config override paths for a UMO. + + Args: + preferences: Shared preferences storage. + umo: Unified message origin. + paths: Dot-separated config path to override value mapping. + """ + await preferences.session_put( + umo, + CORE_CONFIG_OVERRIDE_KEY, + { + "version": CONFIG_OVERRIDE_VERSION, + "paths": normalize_config_override_payload(paths), + }, + ) + + +async def update_core_config_override_paths( + preferences: SharedPreferences, + umo: str, + paths: dict[str, Any], +) -> None: + """Merge core config override paths into the stored payload for a UMO. + + Args: + preferences: Shared preferences storage. + umo: Unified message origin. + paths: Dot-separated config path to override value mapping. + """ + override_payload = await preferences.session_get( + umo, + CORE_CONFIG_OVERRIDE_KEY, + default={}, + ) + merged_paths = normalize_config_override_payload(override_payload) + merged_paths.update(normalize_config_override_payload(paths)) + await save_core_config_override_paths(preferences, umo, merged_paths) + + +async def remove_core_config_override_paths( + preferences: SharedPreferences, + umo: str, + paths: list[str], +) -> None: + """Remove core config override paths from the stored payload for a UMO. + + Args: + preferences: Shared preferences storage. + umo: Unified message origin. + paths: Dot-separated config paths to remove. + """ + override_payload = await preferences.session_get( + umo, + CORE_CONFIG_OVERRIDE_KEY, + default={}, + ) + merged_paths = normalize_config_override_payload(override_payload) + for path in paths: + merged_paths.pop(path, None) + await save_core_config_override_paths(preferences, umo, merged_paths) + + +def apply_config_override_paths( + config_data: dict[str, Any], + paths: dict[str, Any], +) -> dict[str, Any]: + """Apply dot-separated override paths to a config dictionary. + + Args: + config_data: Config data to mutate. + paths: Dot-separated config path to override value mapping. + + Returns: + The mutated config_data object. + """ + for path, value in paths.items(): + target = config_data + parts = [part for part in path.split(".") if part] + if not parts: + continue + for part in parts[:-1]: + child = target.get(part) + if not isinstance(child, dict): + child = {} + target[part] = child + target = child + target[parts[-1]] = copy.deepcopy(value) + return config_data + + +async def build_effective_core_config( + base_config: AstrBotConfig, + preferences: SharedPreferences, + umo: str, +) -> AstrBotConfig: + """Build a per-event effective config by applying UMO overrides. + + Args: + base_config: Shared config profile selected for the event. + preferences: Shared preferences storage. + umo: Unified message origin. + + Returns: + A no-write config view for this single event. + """ + config_data = copy.deepcopy(dict(base_config)) + override_paths = await load_core_config_override_paths(preferences, umo) + apply_config_override_paths(config_data, override_paths) + return EffectiveAstrBotConfig(config_data, base_config=base_config, umo=umo) + + +def build_effective_core_config_sync( + base_config: AstrBotConfig, + preferences: SharedPreferences, + umo: str, +) -> AstrBotConfig: + """Synchronously build a per-UMO effective config. + + Args: + base_config: Shared config profile selected for the UMO. + preferences: Shared preferences storage. + umo: Unified message origin. + + Returns: + A no-write config view for this UMO. + """ + config_data = copy.deepcopy(dict(base_config)) + override_paths = load_core_config_override_paths_sync(preferences, umo) + apply_config_override_paths(config_data, override_paths) + return EffectiveAstrBotConfig(config_data, base_config=base_config, umo=umo) diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index c325a2ea38..936d69f93b 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -432,7 +432,12 @@ async def load_pipeline_scheduler(self) -> dict[str, PipelineScheduler]: mapping = {} for conf_id, ab_config in self.astrbot_config_mgr.confs.items(): scheduler = PipelineScheduler( - PipelineContext(ab_config, self.plugin_manager, conf_id), + PipelineContext( + ab_config, + self.plugin_manager, + conf_id, + self.astrbot_config_mgr, + ), ) await scheduler.initialize() mapping[conf_id] = scheduler @@ -449,7 +454,12 @@ async def reload_pipeline_scheduler(self, conf_id: str) -> None: if not ab_config: raise ValueError(f"配置文件 {conf_id} 不存在") scheduler = PipelineScheduler( - PipelineContext(ab_config, self.plugin_manager, conf_id), + PipelineContext( + ab_config, + self.plugin_manager, + conf_id, + self.astrbot_config_mgr, + ), ) await scheduler.initialize() self.pipeline_scheduler_mapping[conf_id] = scheduler diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index f59f234192..98e1f0dacd 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -312,6 +312,10 @@ async def get_filtered_conversations( base_query = base_query.where( col(ConversationV2.platform_id).in_(platform_ids), ) + if kwargs.get("user_id"): + base_query = base_query.where( + col(ConversationV2.user_id) == str(kwargs["user_id"]), + ) if search_query: search_query = search_query.encode("unicode_escape").decode("utf-8") base_query = base_query.where( diff --git a/astrbot/core/knowledge_base/kb_mgr.py b/astrbot/core/knowledge_base/kb_mgr.py index a211280321..a2532e6469 100644 --- a/astrbot/core/knowledge_base/kb_mgr.py +++ b/astrbot/core/knowledge_base/kb_mgr.py @@ -277,11 +277,15 @@ async def retrieve( kb_ids = [] kb_id_helper_map = {} unavailable_kbs = [] - for kb_name in kb_names: - if kb_helper := await self.get_kb_by_name(kb_name): + for kb_ref in kb_names: + kb_ref = str(kb_ref) + kb_helper = await self.get_kb(kb_ref) + if not kb_helper: + kb_helper = await self.get_kb_by_name(kb_ref) + if kb_helper: if kb_helper.init_error: - unavailable_kbs.append((kb_name, kb_helper.init_error)) - logger.warning(f"知识库 {kb_name} 不可用: {kb_helper.init_error}") + unavailable_kbs.append((kb_ref, kb_helper.init_error)) + logger.warning(f"知识库 {kb_ref} 不可用: {kb_helper.init_error}") continue kb_ids.append(kb_helper.kb.kb_id) kb_id_helper_map[kb_helper.kb.kb_id] = kb_helper diff --git a/astrbot/core/knowledge_base/retrieval/manager.py b/astrbot/core/knowledge_base/retrieval/manager.py index 5b414b171e..154b16a5e4 100644 --- a/astrbot/core/knowledge_base/retrieval/manager.py +++ b/astrbot/core/knowledge_base/retrieval/manager.py @@ -81,7 +81,6 @@ async def retrieve( query: 查询文本 kb_ids: 知识库 ID 列表 top_m_final: 最终返回数量 - enable_rerank: 是否启用 Rerank Returns: List[RetrievalResult]: 检索结果列表 diff --git a/astrbot/core/persona_mgr.py b/astrbot/core/persona_mgr.py index b701648015..d2bfec3204 100644 --- a/astrbot/core/persona_mgr.py +++ b/astrbot/core/persona_mgr.py @@ -1,5 +1,4 @@ from astrbot import logger -from astrbot.api import sp from astrbot.core.astrbot_config_mgr import AstrBotConfigManager from astrbot.core.db import BaseDatabase from astrbot.core.db.po import Persona, PersonaFolder, Personality @@ -86,28 +85,15 @@ async def resolve_selected_persona( tuple: - selected persona_id - selected persona object - - force applied persona_id from session rule + - force applied persona_id, retained for API compatibility - whether use webchat special default persona """ - session_service_config = ( - await sp.get_async( - scope="umo", - scope_id=str(umo), - key="session_service_config", - default={}, - ) - or {} - ) - - force_applied_persona_id = session_service_config.get("persona_id") - persona_id = force_applied_persona_id - - if not persona_id: - persona_id = conversation_persona_id - if persona_id == "[%None]": - pass - elif persona_id is None: - persona_id = (provider_settings or {}).get("default_personality") + force_applied_persona_id = None + persona_id = conversation_persona_id + if persona_id == "[%None]": + pass + elif persona_id is None: + persona_id = (provider_settings or {}).get("default_personality") persona = next( (item for item in self.personas_v3 if item["name"] == persona_id), diff --git a/astrbot/core/pipeline/__init__.py b/astrbot/core/pipeline/__init__.py index 6a6069ff77..a8ffa62f69 100644 --- a/astrbot/core/pipeline/__init__.py +++ b/astrbot/core/pipeline/__init__.py @@ -24,7 +24,6 @@ from .rate_limit_check.stage import RateLimitStage from .respond.stage import RespondStage from .result_decorate.stage import ResultDecorateStage - from .session_status_check.stage import SessionStatusCheckStage from .waking_check.stage import WakingCheckStage from .whitelist_check.stage import WhitelistCheckStage @@ -53,10 +52,6 @@ "astrbot.core.pipeline.result_decorate.stage", "ResultDecorateStage", ), - "SessionStatusCheckStage": ( - "astrbot.core.pipeline.session_status_check.stage", - "SessionStatusCheckStage", - ), "WakingCheckStage": ( "astrbot.core.pipeline.waking_check.stage", "WakingCheckStage", @@ -75,7 +70,6 @@ from .rate_limit_check.stage import RateLimitStage from .respond.stage import RespondStage from .result_decorate.stage import ResultDecorateStage - from .session_status_check.stage import SessionStatusCheckStage from .waking_check.stage import WakingCheckStage from .whitelist_check.stage import WhitelistCheckStage @@ -88,7 +82,6 @@ "RateLimitStage", "RespondStage", "ResultDecorateStage", - "SessionStatusCheckStage", "STAGES_ORDER", "WakingCheckStage", "WhitelistCheckStage", diff --git a/astrbot/core/pipeline/bootstrap.py b/astrbot/core/pipeline/bootstrap.py index 4bb7ceadb7..8dda65a6c9 100644 --- a/astrbot/core/pipeline/bootstrap.py +++ b/astrbot/core/pipeline/bootstrap.py @@ -7,7 +7,6 @@ _BUILTIN_STAGE_MODULES = ( "astrbot.core.pipeline.waking_check.stage", "astrbot.core.pipeline.whitelist_check.stage", - "astrbot.core.pipeline.session_status_check.stage", "astrbot.core.pipeline.rate_limit_check.stage", "astrbot.core.pipeline.content_safety_check.stage", "astrbot.core.pipeline.preprocess_stage.stage", @@ -19,7 +18,6 @@ _EXPECTED_STAGE_NAMES = { "WakingCheckStage", "WhitelistCheckStage", - "SessionStatusCheckStage", "RateLimitStage", "ContentSafetyCheckStage", "PreProcessStage", diff --git a/astrbot/core/pipeline/content_safety_check/stage.py b/astrbot/core/pipeline/content_safety_check/stage.py index 19037eb081..a2e0a1969c 100644 --- a/astrbot/core/pipeline/content_safety_check/stage.py +++ b/astrbot/core/pipeline/content_safety_check/stage.py @@ -17,8 +17,7 @@ class ContentSafetyCheckStage(Stage): """ async def initialize(self, ctx: PipelineContext) -> None: - config = ctx.astrbot_config["content_safety"] - self.strategy_selector = StrategySelector(config) + self.ctx = ctx async def process( self, @@ -27,7 +26,8 @@ async def process( ) -> AsyncGenerator[None, None]: """检查内容安全""" text = check_text if check_text else event.get_message_str() - ok, info = self.strategy_selector.check(text) + strategy_selector = StrategySelector(self.ctx.astrbot_config["content_safety"]) + ok, info = strategy_selector.check(text) if not ok: if event.is_at_or_wake_command: event.set_result( diff --git a/astrbot/core/pipeline/context.py b/astrbot/core/pipeline/context.py index 47cd33b238..100fbd90fe 100644 --- a/astrbot/core/pipeline/context.py +++ b/astrbot/core/pipeline/context.py @@ -8,15 +8,27 @@ from .context_utils import call_event_hook, call_handler if TYPE_CHECKING: + from astrbot.core.astrbot_config_mgr import AstrBotConfigManager from astrbot.core.star import PluginManager @dataclass class PipelineContext: - """上下文对象,包含管道执行所需的上下文信息""" + """Context object with the state needed to execute a pipeline.""" - astrbot_config: AstrBotConfig # AstrBot 配置对象 - plugin_manager: PluginManager # 插件管理器对象 + base_astrbot_config: AstrBotConfig + plugin_manager: PluginManager astrbot_config_id: str + astrbot_config_mgr: AstrBotConfigManager call_handler = call_handler call_event_hook = call_event_hook + + @property + def astrbot_config(self) -> AstrBotConfig: + """Return the current event config view, or the shared base config. + + Returns: + Effective config bound to the current task when a pipeline event is + running; otherwise the shared config profile. + """ + return self.astrbot_config_mgr.get_current_conf(self.base_astrbot_config) diff --git a/astrbot/core/pipeline/preprocess_stage/stage.py b/astrbot/core/pipeline/preprocess_stage/stage.py index c284600a62..4be506deb1 100644 --- a/astrbot/core/pipeline/preprocess_stage/stage.py +++ b/astrbot/core/pipeline/preprocess_stage/stage.py @@ -24,12 +24,8 @@ class PreProcessStage(Stage): async def initialize(self, ctx: PipelineContext) -> None: self.ctx = ctx - self.config = ctx.astrbot_config self.plugin_manager = ctx.plugin_manager - self.stt_settings: dict = self.config.get("provider_stt_settings", {}) - self.platform_settings: dict = self.config.get("platform_settings", {}) - @staticmethod def _track_temp_media(event: AstrMessageEvent, media_path: str) -> None: """Track a media file owned by the current event. @@ -52,11 +48,15 @@ async def process( event: AstrMessageEvent, ) -> None | AsyncGenerator[None, None]: """在处理事件之前的预处理""" + config = self.ctx.astrbot_config + stt_settings: dict = config.get("provider_stt_settings", {}) + platform_settings: dict = config.get("platform_settings", {}) + # 平台特异配置:platform_specific..pre_ack_emoji supported = {"telegram", "lark", "discord"} platform = event.get_platform_name() cfg = ( - self.config.get("platform_specific", {}) + config.get("platform_specific", {}) .get(platform, {}) .get("pre_ack_emoji", {}) ) or {} @@ -73,7 +73,7 @@ async def process( logger.warning(f"{platform} 预回应表情发送失败: {e}") # 路径映射 - if mappings := self.platform_settings.get("path_mapping", []): + if mappings := platform_settings.get("path_mapping", []): # 支持 Record,Image 消息段的路径映射。 message_chain = event.get_messages() @@ -164,7 +164,7 @@ async def process( ) # STT - if self.stt_settings.get("enable", False): + if stt_settings.get("enable", False): # TODO: 独立 ctx = self.plugin_manager.context stt_provider = ctx.get_using_stt_provider(event.unified_msg_origin) diff --git a/astrbot/core/pipeline/process_stage/method/agent_request.py b/astrbot/core/pipeline/process_stage/method/agent_request.py index 9efe538146..3a611938d0 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_request.py +++ b/astrbot/core/pipeline/process_stage/method/agent_request.py @@ -2,7 +2,6 @@ from astrbot.core import logger from astrbot.core.platform.astr_message_event import AstrMessageEvent -from astrbot.core.star.session_llm_manager import SessionServiceManager from ...context import PipelineContext from ..stage import Stage @@ -13,36 +12,33 @@ class AgentRequestSubStage(Stage): async def initialize(self, ctx: PipelineContext) -> None: self.ctx = ctx - self.config = ctx.astrbot_config - - self.bot_wake_prefixs: list[str] = self.config["wake_prefix"] - self.prov_wake_prefix: str = self.config["provider_settings"]["wake_prefix"] - for bwp in self.bot_wake_prefixs: - if self.prov_wake_prefix.startswith(bwp): - logger.info( - f"识别 LLM 聊天额外唤醒前缀 {self.prov_wake_prefix} 以机器人唤醒前缀 {bwp} 开头,已自动去除。", - ) - self.prov_wake_prefix = self.prov_wake_prefix[len(bwp) :] - - agent_runner_type = self.config["provider_settings"]["agent_runner_type"] - if agent_runner_type == "local": - self.agent_sub_stage = InternalAgentSubStage() - else: - self.agent_sub_stage = ThirdPartyAgentSubStage() - await self.agent_sub_stage.initialize(ctx) + self.internal_agent_sub_stage = InternalAgentSubStage() + self.third_party_agent_sub_stage = ThirdPartyAgentSubStage() + await self.internal_agent_sub_stage.initialize(ctx) + await self.third_party_agent_sub_stage.initialize(ctx) async def process(self, event: AstrMessageEvent) -> AsyncGenerator[None, None]: - if not self.ctx.astrbot_config["provider_settings"]["enable"]: + config = self.ctx.astrbot_config + provider_settings = config["provider_settings"] + if not provider_settings["enable"]: logger.debug( "This pipeline does not enable AI capability, skip processing." ) return - if not await SessionServiceManager.should_process_llm_request(event): - logger.debug( - f"The session {event.unified_msg_origin} has disabled AI capability, skipping processing." - ) - return - - async for resp in self.agent_sub_stage.process(event, self.prov_wake_prefix): + bot_wake_prefixes: list[str] = config["wake_prefix"] + provider_wake_prefix: str = provider_settings["wake_prefix"] + for bot_wake_prefix in bot_wake_prefixes: + if provider_wake_prefix.startswith(bot_wake_prefix): + logger.info( + f"识别 LLM 聊天额外唤醒前缀 {provider_wake_prefix} 以机器人唤醒前缀 {bot_wake_prefix} 开头,已自动去除。", + ) + provider_wake_prefix = provider_wake_prefix[len(bot_wake_prefix) :] + + agent_sub_stage = ( + self.internal_agent_sub_stage + if provider_settings["agent_runner_type"] == "local" + else self.third_party_agent_sub_stage + ) + async for resp in agent_sub_stage.process(event, provider_wake_prefix): yield resp diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py index 7b01dd2dc7..f57b335d63 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +++ b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py @@ -53,121 +53,106 @@ class InternalAgentSubStage(Stage): async def initialize(self, ctx: PipelineContext) -> None: self.ctx = ctx - conf = ctx.astrbot_config + self.conv_manager = ctx.plugin_manager.context.conversation_manager + + async def _send_llm_error_message( + self, event: AstrMessageEvent, message: object + ) -> None: + await event.send(MessageChain().message(str(message))) + + async def process( + self, event: AstrMessageEvent, provider_wake_prefix: str + ) -> AsyncGenerator[None, None]: + conf = self.ctx.astrbot_config settings = conf["provider_settings"] - self.streaming_response: bool = settings["streaming_response"] - self.unsupported_streaming_strategy: str = settings[ - "unsupported_streaming_strategy" - ] - self.max_step: int = settings.get("max_agent_step", 30) - self.tool_call_timeout: int = settings.get("tool_call_timeout", 60) - self.tool_schema_mode: str = settings.get("tool_schema_mode", "full") - if self.tool_schema_mode not in ("skills_like", "full"): + streaming_response: bool = settings["streaming_response"] + unsupported_streaming_strategy: str = settings["unsupported_streaming_strategy"] + max_step: int = settings.get("max_agent_step", 30) + tool_call_timeout: int = settings.get("tool_call_timeout", 60) + tool_schema_mode: str = settings.get("tool_schema_mode", "full") + if tool_schema_mode not in ("skills_like", "full"): logger.warning( - "Unsupported tool_schema_mode: %s, fallback to skills_like", - self.tool_schema_mode, + "Unsupported tool_schema_mode: %s, fallback to full", + tool_schema_mode, ) - self.tool_schema_mode = "full" - if isinstance(self.max_step, bool): # workaround: #2622 - self.max_step = 30 - self.show_tool_use: bool = settings.get("show_tool_use_status", True) - self.show_tool_call_result: bool = settings.get("show_tool_call_result", False) - self.buffer_intermediate_messages: bool = settings.get( + tool_schema_mode = "full" + if isinstance(max_step, bool): # workaround: #2622 + max_step = 30 + show_tool_use: bool = settings.get("show_tool_use_status", True) + show_tool_call_result: bool = settings.get("show_tool_call_result", False) + buffer_intermediate_messages: bool = settings.get( "buffer_intermediate_messages", False, ) - self.show_reasoning = settings.get("display_reasoning_text", False) - self.sanitize_context_by_modalities: bool = settings.get( + show_reasoning = settings.get("display_reasoning_text", False) + sanitize_context_by_modalities: bool = settings.get( "sanitize_context_by_modalities", False, ) - self.kb_agentic_mode: bool = conf.get("kb_agentic_mode", False) + kb_agentic_mode: bool = conf.get("kb_agentic_mode", False) file_extract_conf: dict = settings.get("file_extract", {}) - self.file_extract_enabled: bool = file_extract_conf.get("enable", False) - self.file_extract_prov: str = file_extract_conf.get("provider", "moonshotai") - self.file_extract_msh_api_key: str = file_extract_conf.get( - "moonshotai_api_key", "" - ) + file_extract_enabled: bool = file_extract_conf.get("enable", False) + file_extract_prov: str = file_extract_conf.get("provider", "moonshotai") + file_extract_msh_api_key: str = file_extract_conf.get("moonshotai_api_key", "") - # 上下文管理相关 - self.context_limit_reached_strategy: str = settings.get( + context_limit_reached_strategy: str = settings.get( "context_limit_reached_strategy", "truncate_by_turns" ) - self.llm_compress_instruction: str = settings.get( - "llm_compress_instruction", "" - ) - self.llm_compress_keep_recent_ratio: float = settings.get( + llm_compress_instruction: str = settings.get("llm_compress_instruction", "") + llm_compress_keep_recent_ratio: float = settings.get( "llm_compress_keep_recent_ratio", 0.15 ) - self.llm_compress_provider_id: str = settings.get( - "llm_compress_provider_id", "" - ) - self.max_context_length = settings["max_context_length"] # int - self.dequeue_context_length: int = min( + llm_compress_provider_id: str = settings.get("llm_compress_provider_id", "") + max_context_length = settings["max_context_length"] + dequeue_context_length: int = min( max(1, settings["dequeue_context_length"]), - self.max_context_length - 1, + max_context_length - 1, ) - if self.dequeue_context_length <= 0: - self.dequeue_context_length = 1 - self.fallback_max_context_tokens: int = settings.get( + if dequeue_context_length <= 0: + dequeue_context_length = 1 + fallback_max_context_tokens: int = settings.get( "fallback_max_context_tokens", 128000 ) - self.llm_safety_mode = settings.get("llm_safety_mode", True) - self.safety_mode_strategy = settings.get( - "safety_mode_strategy", "system_prompt" - ) - - self.computer_use_runtime = settings.get("computer_use_runtime") - self.sandbox_cfg = settings.get("sandbox", {}) - - # Proactive capability configuration + llm_safety_mode = settings.get("llm_safety_mode", True) + safety_mode_strategy = settings.get("safety_mode_strategy", "system_prompt") + computer_use_runtime = settings.get("computer_use_runtime") + sandbox_cfg = settings.get("sandbox", {}) proactive_cfg = settings.get("proactive_capability", {}) - self.add_cron_tools = proactive_cfg.get("add_cron_tools", True) - - self.conv_manager = ctx.plugin_manager.context.conversation_manager - - self.main_agent_cfg = MainAgentBuildConfig( - tool_call_timeout=self.tool_call_timeout, - tool_schema_mode=self.tool_schema_mode, - sanitize_context_by_modalities=self.sanitize_context_by_modalities, - kb_agentic_mode=self.kb_agentic_mode, - file_extract_enabled=self.file_extract_enabled, - file_extract_prov=self.file_extract_prov, - file_extract_msh_api_key=self.file_extract_msh_api_key, - context_limit_reached_strategy=self.context_limit_reached_strategy, - llm_compress_instruction=self.llm_compress_instruction, - llm_compress_keep_recent_ratio=self.llm_compress_keep_recent_ratio, - llm_compress_provider_id=self.llm_compress_provider_id, - max_context_length=self.max_context_length, - dequeue_context_length=self.dequeue_context_length, - fallback_max_context_tokens=self.fallback_max_context_tokens, - llm_safety_mode=self.llm_safety_mode, - safety_mode_strategy=self.safety_mode_strategy, - computer_use_runtime=self.computer_use_runtime, - sandbox_cfg=self.sandbox_cfg, - add_cron_tools=self.add_cron_tools, + add_cron_tools = proactive_cfg.get("add_cron_tools", True) + + main_agent_cfg = MainAgentBuildConfig( + tool_call_timeout=tool_call_timeout, + tool_schema_mode=tool_schema_mode, + sanitize_context_by_modalities=sanitize_context_by_modalities, + kb_agentic_mode=kb_agentic_mode, + file_extract_enabled=file_extract_enabled, + file_extract_prov=file_extract_prov, + file_extract_msh_api_key=file_extract_msh_api_key, + context_limit_reached_strategy=context_limit_reached_strategy, + llm_compress_instruction=llm_compress_instruction, + llm_compress_keep_recent_ratio=llm_compress_keep_recent_ratio, + llm_compress_provider_id=llm_compress_provider_id, + max_context_length=max_context_length, + dequeue_context_length=dequeue_context_length, + fallback_max_context_tokens=fallback_max_context_tokens, + llm_safety_mode=llm_safety_mode, + safety_mode_strategy=safety_mode_strategy, + computer_use_runtime=computer_use_runtime, + sandbox_cfg=sandbox_cfg, + add_cron_tools=add_cron_tools, provider_settings=settings, subagent_orchestrator=conf.get("subagent_orchestrator", {}), timezone=self.ctx.plugin_manager.context.get_config().get("timezone"), max_quoted_fallback_images=settings.get("max_quoted_fallback_images", 20), ) - async def _send_llm_error_message( - self, event: AstrMessageEvent, message: object - ) -> None: - await event.send(MessageChain().message(str(message))) - - async def process( - self, event: AstrMessageEvent, provider_wake_prefix: str - ) -> AsyncGenerator[None, None]: follow_up_capture: FollowUpCapture | None = None follow_up_consumed_marked = False follow_up_activated = False typing_requested = False try: - streaming_response = self.streaming_response if (enable_streaming := event.get_extra("enable_streaming")) is not None: streaming_response = bool(enable_streaming) @@ -219,7 +204,7 @@ async def process( runner_registered = False try: build_cfg = replace( - self.main_agent_cfg, + main_agent_cfg, provider_wake_prefix=provider_wake_prefix, streaming_response=streaming_response, ) @@ -258,7 +243,7 @@ async def process( return stream_to_general = ( - self.unsupported_streaming_strategy == "turn_off" + unsupported_streaming_strategy == "turn_off" and not event.platform_meta.support_streaming_message ) @@ -311,11 +296,11 @@ async def process( run_live_agent( agent_runner, tts_provider, - self.max_step, - self.show_tool_use, - self.show_tool_call_result, - show_reasoning=self.show_reasoning, - buffer_intermediate_messages=self.buffer_intermediate_messages, + max_step, + show_tool_use, + show_tool_call_result, + show_reasoning=show_reasoning, + buffer_intermediate_messages=buffer_intermediate_messages, ), ), ) @@ -342,11 +327,11 @@ async def process( .set_async_stream( run_agent( agent_runner, - self.max_step, - self.show_tool_use, - self.show_tool_call_result, - show_reasoning=self.show_reasoning, - buffer_intermediate_messages=self.buffer_intermediate_messages, + max_step, + show_tool_use, + show_tool_call_result, + show_reasoning=show_reasoning, + buffer_intermediate_messages=buffer_intermediate_messages, ), ), ) @@ -372,12 +357,12 @@ async def process( else: async for _ in run_agent( agent_runner, - self.max_step, - self.show_tool_use, - self.show_tool_call_result, + max_step, + show_tool_use, + show_tool_call_result, stream_to_general, - show_reasoning=self.show_reasoning, - buffer_intermediate_messages=self.buffer_intermediate_messages, + show_reasoning=show_reasoning, + buffer_intermediate_messages=buffer_intermediate_messages, ): yield diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py index 9ab315779c..8cdb8e0994 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +++ b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py @@ -164,27 +164,6 @@ async def _close_runner_if_supported(runner: "BaseAgentRunner") -> None: class ThirdPartyAgentSubStage(Stage): async def initialize(self, ctx: PipelineContext) -> None: self.ctx = ctx - self.conf = ctx.astrbot_config - self.runner_type = self.conf["provider_settings"]["agent_runner_type"] - self.prov_id = self.conf["provider_settings"].get( - AGENT_RUNNER_TYPE_KEY.get(self.runner_type, ""), - "", - ) - settings = ctx.astrbot_config["provider_settings"] - self.streaming_response: bool = settings["streaming_response"] - self.unsupported_streaming_strategy: str = settings[ - "unsupported_streaming_strategy" - ] - self.stream_consumption_close_timeout_sec: int = coerce_int_config( - settings.get( - "third_party_stream_consumption_close_timeout_sec", - STREAM_CONSUMPTION_CLOSE_TIMEOUT_SEC, - ), - default=STREAM_CONSUMPTION_CLOSE_TIMEOUT_SEC, - min_value=1, - field_name="third_party_stream_consumption_close_timeout_sec", - source="Third-party runner config", - ) async def _resolve_persona_custom_error_message( self, event: AstrMessageEvent @@ -197,7 +176,7 @@ async def _resolve_persona_custom_error_message( return await resolve_persona_custom_error_message( event=event, persona_manager=self.ctx.plugin_manager.context.persona_manager, - provider_settings=self.conf["provider_settings"], + provider_settings=self.ctx.astrbot_config["provider_settings"], conversation_persona_id=conversation_persona_id, ) except Exception as e: @@ -290,22 +269,45 @@ async def process( self, event: AstrMessageEvent, provider_wake_prefix: str ) -> AsyncGenerator[None, None]: req: ProviderRequest | None = None + conf = self.ctx.astrbot_config + settings = conf["provider_settings"] + runner_type = settings["agent_runner_type"] + provider_id = settings.get( + AGENT_RUNNER_TYPE_KEY.get(runner_type, ""), + "", + ) + streaming_response: bool = settings["streaming_response"] + unsupported_streaming_strategy: str = settings["unsupported_streaming_strategy"] + stream_consumption_close_timeout_sec: int = coerce_int_config( + settings.get( + "third_party_stream_consumption_close_timeout_sec", + STREAM_CONSUMPTION_CLOSE_TIMEOUT_SEC, + ), + default=STREAM_CONSUMPTION_CLOSE_TIMEOUT_SEC, + min_value=1, + field_name="third_party_stream_consumption_close_timeout_sec", + source="Third-party runner config", + ) if provider_wake_prefix and not event.message_str.startswith( provider_wake_prefix ): return - self.prov_cfg: dict = next( - (p for p in astrbot_config["provider"] if p["id"] == self.prov_id), + provider_config: dict = next( + ( + p + for p in conf.get("provider", astrbot_config["provider"]) + if p["id"] == provider_id + ), {}, ) - if not self.prov_id: + if not provider_id: logger.error("没有填写 Agent Runner 提供商 ID,请前往配置页面配置。") return - if not self.prov_cfg: + if not provider_config: logger.error( - f"Agent Runner 提供商 {self.prov_id} 配置不存在,请前往配置页面修改配置。" + f"Agent Runner 提供商 {provider_id} 配置不存在,请前往配置页面修改配置。" ) return @@ -331,17 +333,17 @@ async def process( if await call_event_hook(event, EventType.OnLLMRequestEvent, req): return - if self.runner_type == "dify": + if runner_type == "dify": runner = DifyAgentRunner[AstrAgentContext]() - elif self.runner_type == "coze": + elif runner_type == "coze": runner = CozeAgentRunner[AstrAgentContext]() - elif self.runner_type == "dashscope": + elif runner_type == "dashscope": runner = DashscopeAgentRunner[AstrAgentContext]() - elif self.runner_type == DEERFLOW_PROVIDER_TYPE: + elif runner_type == DEERFLOW_PROVIDER_TYPE: runner = DeerFlowAgentRunner[AstrAgentContext]() else: raise ValueError( - f"Unsupported third party agent runner type: {self.runner_type}", + f"Unsupported third party agent runner type: {runner_type}", ) astr_agent_ctx = AstrAgentContext( @@ -349,12 +351,11 @@ async def process( event=event, ) - streaming_response = self.streaming_response if (enable_streaming := event.get_extra("enable_streaming")) is not None: streaming_response = bool(enable_streaming) stream_to_general = ( - self.unsupported_streaming_strategy == "turn_off" + unsupported_streaming_strategy == "turn_off" and not event.platform_meta.support_streaming_message ) streaming_used = streaming_response and not stream_to_general @@ -384,13 +385,13 @@ def mark_stream_consumed() -> None: tool_call_timeout=120, ), agent_hooks=MAIN_AGENT_HOOKS, - provider_config=self.prov_cfg, + provider_config=provider_config, streaming=streaming_response, ) if streaming_used: stream_watchdog_task = _start_stream_watchdog( - timeout_sec=self.stream_consumption_close_timeout_sec, + timeout_sec=stream_consumption_close_timeout_sec, is_stream_consumed=lambda: stream_consumed, close_runner_once=close_runner_once, ) @@ -423,7 +424,7 @@ def mark_stream_consumed() -> None: asyncio.create_task( Metric.upload( llm_tick=1, - model_name=self.runner_type, - provider_type=self.runner_type, + model_name=runner_type, + provider_type=runner_type, ), ) diff --git a/astrbot/core/pipeline/process_stage/method/star_request.py b/astrbot/core/pipeline/process_stage/method/star_request.py index 3adcddc077..e9f662578c 100644 --- a/astrbot/core/pipeline/process_stage/method/star_request.py +++ b/astrbot/core/pipeline/process_stage/method/star_request.py @@ -16,8 +16,6 @@ class StarRequestSubStage(Stage): async def initialize(self, ctx: PipelineContext) -> None: - self.prompt_prefix = ctx.astrbot_config["provider_settings"]["prompt_prefix"] - self.identifier = ctx.astrbot_config["provider_settings"]["identifier"] self.ctx = ctx async def process( diff --git a/astrbot/core/pipeline/process_stage/stage.py b/astrbot/core/pipeline/process_stage/stage.py index 076f7f12ac..d0f757a7a3 100644 --- a/astrbot/core/pipeline/process_stage/stage.py +++ b/astrbot/core/pipeline/process_stage/stage.py @@ -14,7 +14,6 @@ class ProcessStage(Stage): async def initialize(self, ctx: PipelineContext) -> None: self.ctx = ctx - self.config = ctx.astrbot_config self.plugin_manager = ctx.plugin_manager # initialize agent sub stage diff --git a/astrbot/core/pipeline/rate_limit_check/stage.py b/astrbot/core/pipeline/rate_limit_check/stage.py index d8b2b068ae..dee8f28882 100644 --- a/astrbot/core/pipeline/rate_limit_check/stage.py +++ b/astrbot/core/pipeline/rate_limit_check/stage.py @@ -24,21 +24,14 @@ def __init__(self) -> None: self.event_timestamps: defaultdict[str, deque[datetime]] = defaultdict(deque) # 为每个会话设置一个锁,避免并发冲突 self.locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock) - # 限流参数 - self.rate_limit_count: int = 0 - self.rate_limit_time: timedelta = timedelta(0) async def initialize(self, ctx: PipelineContext) -> None: - """初始化限流器,根据配置设置限流参数。""" - self.rate_limit_count = ctx.astrbot_config["platform_settings"]["rate_limit"][ - "count" - ] - self.rate_limit_time = timedelta( - seconds=ctx.astrbot_config["platform_settings"]["rate_limit"]["time"], - ) - self.rl_strategy = ctx.astrbot_config["platform_settings"]["rate_limit"][ - "strategy" - ] # stall or discard + """Initialize the rate limiter state. + + Args: + ctx: Pipeline context used to read config during event processing. + """ + self.ctx = ctx async def process( self, @@ -56,22 +49,26 @@ async def process( """ session_id = event.session_id now = datetime.now() + rate_limit = self.ctx.astrbot_config["platform_settings"]["rate_limit"] + rate_limit_count = rate_limit["count"] + rate_limit_time = timedelta(seconds=rate_limit["time"]) + rl_strategy = rate_limit["strategy"] async with self.locks[session_id]: # 确保同一会话不会并发修改队列 # 检查并处理限流,可能需要多次检查直到满足条件 while True: timestamps = self.event_timestamps[session_id] - self._remove_expired_timestamps(timestamps, now) + self._remove_expired_timestamps(timestamps, now, rate_limit_time) - if self.rate_limit_count <= 0: + if rate_limit_count <= 0: break - if len(timestamps) < self.rate_limit_count: + if len(timestamps) < rate_limit_count: timestamps.append(now) break - next_window_time = timestamps[0] + self.rate_limit_time + next_window_time = timestamps[0] + rate_limit_time stall_duration = (next_window_time - now).total_seconds() + 0.3 - match self.rl_strategy: + match rl_strategy: case RateLimitStrategy.STALL.value: logger.info( f"会话 {session_id} 被限流。根据限流策略,此会话处理将被暂停 {stall_duration:.2f} 秒。", @@ -88,14 +85,16 @@ def _remove_expired_timestamps( self, timestamps: deque[datetime], now: datetime, + rate_limit_time: timedelta, ) -> None: """移除时间窗口外的时间戳。 Args: timestamps (Deque[datetime]): 当前会话的时间戳队列。 now (datetime): 当前时间,用于计算过期时间。 + rate_limit_time: Current rate-limit window duration. """ - expiry_threshold: datetime = now - self.rate_limit_time + expiry_threshold: datetime = now - rate_limit_time while timestamps and timestamps[0] < expiry_threshold: timestamps.popleft() diff --git a/astrbot/core/pipeline/respond/stage.py b/astrbot/core/pipeline/respond/stage.py index 888fb65a4b..16220b1edc 100644 --- a/astrbot/core/pipeline/respond/stage.py +++ b/astrbot/core/pipeline/respond/stage.py @@ -51,41 +51,6 @@ class RespondStage(Stage): async def initialize(self, ctx: PipelineContext) -> None: self.ctx = ctx - self.config = ctx.astrbot_config - self.platform_settings: dict = self.config.get("platform_settings", {}) - - self.reply_with_mention = ctx.astrbot_config["platform_settings"][ - "reply_with_mention" - ] - self.reply_with_quote = ctx.astrbot_config["platform_settings"][ - "reply_with_quote" - ] - - # 分段回复 - self.enable_seg: bool = ctx.astrbot_config["platform_settings"][ - "segmented_reply" - ]["enable"] - self.only_llm_result = ctx.astrbot_config["platform_settings"][ - "segmented_reply" - ]["only_llm_result"] - - self.interval_method = ctx.astrbot_config["platform_settings"][ - "segmented_reply" - ]["interval_method"] - self.log_base = float( - ctx.astrbot_config["platform_settings"]["segmented_reply"]["log_base"], - ) - self.interval = [1.5, 3.5] - if self.enable_seg: - interval_str: str = ctx.astrbot_config["platform_settings"][ - "segmented_reply" - ]["interval"] - interval_str_ls = interval_str.replace(" ", "").split(",") - try: - self.interval = [float(t) for t in interval_str_ls] - except BaseException as e: - logger.error(f"解析分段回复的间隔时间失败。{e}") - logger.info(f"分段回复间隔时间:{self.interval}") async def _word_cnt(self, text: str) -> int: """分段回复 统计字数""" @@ -95,16 +60,22 @@ async def _word_cnt(self, text: str) -> int: word_count = len([c for c in text if c.isalnum()]) return word_count - async def _calc_comp_interval(self, comp: BaseMessageComponent) -> float: + async def _calc_comp_interval( + self, + comp: BaseMessageComponent, + interval_method: str, + log_base: float, + interval: list[float], + ) -> float: """分段回复 计算间隔时间""" - if self.interval_method == "log": + if interval_method == "log": if isinstance(comp, Comp.Plain): wc = await self._word_cnt(comp.text) - i = math.log(wc + 1, self.log_base) + i = math.log(wc + 1, log_base) return random.uniform(i, i + 0.5) return random.uniform(1, 1.75) # random - return random.uniform(self.interval[0], self.interval[1]) + return random.uniform(interval[0], interval[1]) async def _is_empty_message_chain(self, chain: list[BaseMessageComponent]) -> bool: """检查消息链是否为空 @@ -127,14 +98,19 @@ async def _is_empty_message_chain(self, chain: list[BaseMessageComponent]) -> bo # 如果所有组件都为空 return True - def is_seg_reply_required(self, event: AstrMessageEvent) -> bool: + def is_seg_reply_required( + self, + event: AstrMessageEvent, + enable_seg: bool, + only_llm_result: bool, + ) -> bool: """检查是否需要分段回复""" - if not self.enable_seg: + if not enable_seg: return False if (result := event.get_result()) is None: return False - if self.only_llm_result and not result.is_model_result(): + if only_llm_result and not result.is_model_result(): return False if event.get_platform_name() in [ @@ -170,6 +146,23 @@ async def process( self, event: AstrMessageEvent, ) -> None | AsyncGenerator[None, None]: + config = self.ctx.astrbot_config + platform_settings: dict = config.get("platform_settings", {}) + segmented_reply = platform_settings.get("segmented_reply", {}) + enable_seg: bool = segmented_reply.get("enable", False) + only_llm_result = segmented_reply.get("only_llm_result", True) + interval_method = segmented_reply.get("interval_method", "random") + log_base = float(segmented_reply.get("log_base", 2.6)) + interval = [1.5, 3.5] + if enable_seg: + interval_str: str = segmented_reply.get("interval", "1.5,3.5") + interval_str_ls = interval_str.replace(" ", "").split(",") + try: + interval = [float(t) for t in interval_str_ls] + except BaseException as e: + logger.error(f"解析分段回复的间隔时间失败。{e}") + logger.debug(f"分段回复间隔时间:{interval}") + result = event.get_result() if result is None: return @@ -213,7 +206,7 @@ async def process( return # 流式结果直接交付平台适配器处理 realtime_segmenting = ( - self.config.get("provider_settings", {}).get( + config.get("provider_settings", {}).get( "unsupported_streaming_strategy", "realtime_segmenting", ) @@ -224,7 +217,7 @@ async def process( return if len(result.chain) > 0: # 检查路径映射 - if mappings := self.platform_settings.get("path_mapping", []): + if mappings := platform_settings.get("path_mapping", []): for idx, component in enumerate(result.chain): if isinstance(component, Comp.File) and component.file: # 支持 File 消息段的路径映射。 @@ -252,7 +245,7 @@ async def process( # 发送消息链 # Record 需要强制单独发送 need_separately = {ComponentType.Record} - if self.is_seg_reply_required(event): + if self.is_seg_reply_required(event, enable_seg, only_llm_result): header_comps = self._extract_comp( result.chain, {ComponentType.Reply, ComponentType.At}, @@ -265,7 +258,12 @@ async def process( ) return for comp in result.chain: - i = await self._calc_comp_interval(comp) + i = await self._calc_comp_interval( + comp, + interval_method, + log_base, + interval, + ) await asyncio.sleep(i) try: if comp.type in need_separately: diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index 5956c8b5ef..49dfa18be4 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -10,7 +10,6 @@ from astrbot.core.pipeline.content_safety_check.stage import ContentSafetyCheckStage from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.platform.message_type import MessageType -from astrbot.core.star.session_llm_manager import SessionServiceManager from astrbot.core.star.star import star_map from astrbot.core.star.star_handler import EventType, star_handlers_registry @@ -22,98 +21,30 @@ class ResultDecorateStage(Stage): async def initialize(self, ctx: PipelineContext) -> None: self.ctx = ctx - self.reply_prefix = ctx.astrbot_config["platform_settings"]["reply_prefix"] - self.reply_with_mention = ctx.astrbot_config["platform_settings"][ - "reply_with_mention" - ] - self.reply_with_quote = ctx.astrbot_config["platform_settings"][ - "reply_with_quote" - ] - self.t2i_word_threshold = ctx.astrbot_config["t2i_word_threshold"] - try: - self.t2i_word_threshold = int(self.t2i_word_threshold) - self.t2i_word_threshold = max(self.t2i_word_threshold, 50) - except BaseException: - self.t2i_word_threshold = 150 - self.t2i_strategy = ctx.astrbot_config["t2i_strategy"] - self.t2i_use_network = self.t2i_strategy == "remote" - self.t2i_active_template = ctx.astrbot_config["t2i_active_template"] - - self.forward_threshold = ctx.astrbot_config["platform_settings"][ - "forward_threshold" - ] - - trigger_probability = ctx.astrbot_config["provider_tts_settings"].get( - "trigger_probability", - 1, - ) - try: - self.tts_trigger_probability = max( - 0.0, - min(float(trigger_probability), 1.0), - ) - except (TypeError, ValueError): - self.tts_trigger_probability = 1.0 - - # 分段回复 - self.words_count_threshold = int( - ctx.astrbot_config["platform_settings"]["segmented_reply"][ - "words_count_threshold" - ], - ) - self.enable_segmented_reply = ctx.astrbot_config["platform_settings"][ - "segmented_reply" - ]["enable"] - self.only_llm_result = ctx.astrbot_config["platform_settings"][ - "segmented_reply" - ]["only_llm_result"] - self.split_mode = ctx.astrbot_config["platform_settings"][ - "segmented_reply" - ].get("split_mode", "regex") - self.regex = ctx.astrbot_config["platform_settings"]["segmented_reply"]["regex"] - self.split_words = ctx.astrbot_config["platform_settings"][ - "segmented_reply" - ].get("split_words", ["。", "?", "!", "~", "…"]) - if self.split_words: - escaped_words = sorted( - [re.escape(word) for word in self.split_words], key=len, reverse=True - ) - self.split_words_pattern = re.compile( - f"(.*?({'|'.join(escaped_words)})|.+$)", re.DOTALL - ) - else: - self.split_words_pattern = None - self.content_cleanup_rule = ctx.astrbot_config["platform_settings"][ - "segmented_reply" - ]["content_cleanup_rule"] - - # exception - self.content_safe_check_reply = ctx.astrbot_config["content_safety"][ - "also_use_in_response" - ] self.content_safe_check_stage = None - if self.content_safe_check_reply: - for stage_cls in registered_stages: - if stage_cls.__name__ == "ContentSafetyCheckStage": - self.content_safe_check_stage = stage_cls() - await self.content_safe_check_stage.initialize(ctx) + for stage_cls in registered_stages: + if stage_cls.__name__ == "ContentSafetyCheckStage": + self.content_safe_check_stage = stage_cls() + await self.content_safe_check_stage.initialize(ctx) - provider_cfg = ctx.astrbot_config.get("provider_settings", {}) - self.show_reasoning = provider_cfg.get("display_reasoning_text", False) - - def _split_text_by_words(self, text: str) -> list[str]: + def _split_text_by_words( + self, + text: str, + split_words_pattern: re.Pattern[str] | None, + split_words: list[str], + ) -> list[str]: """使用分段词列表分段文本""" - if not self.split_words_pattern: + if not split_words_pattern: return [text] - segments = self.split_words_pattern.findall(text) + segments = split_words_pattern.findall(text) result = [] for seg in segments: if isinstance(seg, tuple): content = seg[0] if not isinstance(content, str): continue - for word in self.split_words: + for word in split_words: if content.endswith(word): content = content[: -len(word)] break @@ -135,10 +66,64 @@ async def process( return is_stream = result.result_content_type == ResultContentType.STREAMING_FINISH + config = self.ctx.astrbot_config + platform_settings = config.get("platform_settings", {}) + segmented_reply = platform_settings.get("segmented_reply", {}) + reply_prefix = platform_settings.get("reply_prefix", "") + reply_with_mention = platform_settings.get("reply_with_mention", False) + reply_with_quote = platform_settings.get("reply_with_quote", False) + forward_threshold = platform_settings.get("forward_threshold", 1500) + t2i_word_threshold = config.get("t2i_word_threshold", 150) + try: + t2i_word_threshold = max(int(t2i_word_threshold), 50) + except BaseException: + t2i_word_threshold = 150 + t2i_strategy = config.get("t2i_strategy", "local") + t2i_use_network = t2i_strategy == "remote" + t2i_active_template = config.get("t2i_active_template", "") + + tts_settings = config.get("provider_tts_settings", {}) + trigger_probability = tts_settings.get("trigger_probability", 1) + try: + tts_trigger_probability = max( + 0.0, + min(float(trigger_probability), 1.0), + ) + except (TypeError, ValueError): + tts_trigger_probability = 1.0 + + words_count_threshold = int(segmented_reply.get("words_count_threshold", 150)) + enable_segmented_reply = segmented_reply.get("enable", False) + only_llm_result = segmented_reply.get("only_llm_result", True) + split_mode = segmented_reply.get("split_mode", "regex") + split_regex = segmented_reply.get("regex", r".*?[。?!~…]+|.+$") + split_words = segmented_reply.get( + "split_words", + ["。", "?", "!", "~", "…"], + ) + if split_words: + escaped_words = sorted( + [re.escape(word) for word in split_words], + key=len, + reverse=True, + ) + split_words_pattern = re.compile( + f"(.*?({'|'.join(escaped_words)})|.+$)", + re.DOTALL, + ) + else: + split_words_pattern = None + content_cleanup_rule = segmented_reply.get("content_cleanup_rule", "") + content_safe_check_reply = config.get("content_safety", {}).get( + "also_use_in_response", + False, + ) + provider_cfg = config.get("provider_settings", {}) + show_reasoning = provider_cfg.get("display_reasoning_text", False) # 回复时检查内容安全 if ( - self.content_safe_check_reply + content_safe_check_reply and self.content_safe_check_stage and result.is_llm_result() and not is_stream # 流式输出不检查内容安全 @@ -195,36 +180,40 @@ async def process( if len(result.chain) > 0: # 回复前缀 - if self.reply_prefix: + if reply_prefix: for comp in result.chain: if isinstance(comp, Plain): - comp.text = self.reply_prefix + comp.text + comp.text = reply_prefix + comp.text break # 分段回复 - if self.enable_segmented_reply and event.get_platform_name() not in [ + if enable_segmented_reply and event.get_platform_name() not in [ "qq_official_webhook", "weixin_official_account", "dingtalk", ]: if ( - self.only_llm_result and result.is_model_result() - ) or not self.only_llm_result: + only_llm_result and result.is_model_result() + ) or not only_llm_result: new_chain = [] for comp in result.chain: if isinstance(comp, Plain): - if len(comp.text) > self.words_count_threshold: + if len(comp.text) > words_count_threshold: # 不分段回复 new_chain.append(comp) continue # 根据 split_mode 选择分段方式 - if self.split_mode == "words": - split_response = self._split_text_by_words(comp.text) + if split_mode == "words": + split_response = self._split_text_by_words( + comp.text, + split_words_pattern, + split_words, + ) else: # regex 模式 try: split_response = re.findall( - self.regex, + split_regex, comp.text, re.DOTALL | re.MULTILINE, ) @@ -242,8 +231,8 @@ async def process( new_chain.append(comp) continue for seg in split_response: - if self.content_cleanup_rule: - seg = re.sub(self.content_cleanup_rule, "", seg) + if content_cleanup_rule: + seg = re.sub(content_cleanup_rule, "", seg) seg = seg.strip() if seg: new_chain.append(Plain(seg)) @@ -258,10 +247,9 @@ async def process( ) should_tts = ( - bool(self.ctx.astrbot_config["provider_tts_settings"]["enable"]) + bool(tts_settings.get("enable", False)) and result.is_llm_result() - and await SessionServiceManager.should_process_tts_request(event) - and random.random() <= self.tts_trigger_probability + and random.random() <= tts_trigger_probability and tts_provider ) if should_tts and not tts_provider: @@ -271,7 +259,7 @@ async def process( if ( not should_tts - and self.show_reasoning + and show_reasoning and event.get_extra("_llm_reasoning_content") ): # inject reasoning content to chain @@ -310,15 +298,12 @@ async def process( event.track_temporary_local_file(audio_path) - use_file_service = self.ctx.astrbot_config[ - "provider_tts_settings" - ]["use_file_service"] - callback_api_base = self.ctx.astrbot_config[ - "callback_api_base" - ] - dual_output = self.ctx.astrbot_config[ - "provider_tts_settings" - ]["dual_output"] + use_file_service = tts_settings.get( + "use_file_service", + False, + ) + callback_api_base = config.get("callback_api_base", "") + dual_output = tts_settings.get("dual_output", False) url = None if use_file_service and callback_api_base: @@ -347,7 +332,7 @@ async def process( # 文本转图片 elif ( - result.use_t2i_ is None and self.ctx.astrbot_config["t2i"] + result.use_t2i_ is None and config.get("t2i", False) ) or result.use_t2i_: parts = [] for comp in result.chain: @@ -356,14 +341,14 @@ async def process( else: break plain_str = "".join(parts) - if plain_str and len(plain_str) > self.t2i_word_threshold: + if plain_str and len(plain_str) > t2i_word_threshold: render_start = time.time() try: url = await html_renderer.render_t2i( plain_str, return_url=True, - use_network=self.t2i_use_network, - template_name=self.t2i_active_template, + use_network=t2i_use_network, + template_name=t2i_active_template, ) except BaseException: logger.error("文本转图片失败,使用文本发送。") @@ -375,12 +360,11 @@ async def process( if url: if url.startswith("http"): result.chain = [Image.fromURL(url)] - elif ( - self.ctx.astrbot_config["t2i_use_file_service"] - and self.ctx.astrbot_config["callback_api_base"] + elif config.get("t2i_use_file_service", False) and config.get( + "callback_api_base", "" ): token = await file_token_service.register_file(url) - url = f"{self.ctx.astrbot_config['callback_api_base']}/api/file/{token}" + url = f"{config['callback_api_base']}/api/file/{token}" logger.debug(f"已注册:{url}") result.chain = [Image.fromURL(url)] else: @@ -392,7 +376,7 @@ async def process( for comp in result.chain: if isinstance(comp, Plain): word_cnt += len(comp.text) - if word_cnt > self.forward_threshold: + if word_cnt > forward_threshold: node = Node( uin=event.get_self_id(), name="AstrBot", @@ -407,7 +391,7 @@ async def process( if can_decorate: # at 回复 if ( - self.reply_with_mention + reply_with_mention and event.get_message_type() != MessageType.FRIEND_MESSAGE ): result.chain.insert( @@ -418,5 +402,5 @@ async def process( result.chain[1].text = "\n" + result.chain[1].text # 引用回复 - if self.reply_with_quote: + if reply_with_quote: result.chain.insert(0, Reply(id=event.message_obj.message_id)) diff --git a/astrbot/core/pipeline/scheduler.py b/astrbot/core/pipeline/scheduler.py index f62dc5d899..ba19cb161a 100644 --- a/astrbot/core/pipeline/scheduler.py +++ b/astrbot/core/pipeline/scheduler.py @@ -83,6 +83,14 @@ async def execute(self, event: AstrMessageEvent) -> None: """ active_event_registry.register(event) + effective_config = await self.ctx.astrbot_config_mgr.build_effective_conf( + event.unified_msg_origin, + self.ctx.base_astrbot_config, + ) + config_token = self.ctx.astrbot_config_mgr.activate_effective_conf( + event.unified_msg_origin, + effective_config, + ) try: await self._process_stages(event) @@ -92,5 +100,6 @@ async def execute(self, event: AstrMessageEvent) -> None: logger.debug("pipeline execution completed.") finally: + self.ctx.astrbot_config_mgr.reset_effective_conf(config_token) event.cleanup_temporary_local_files() active_event_registry.unregister(event) diff --git a/astrbot/core/pipeline/session_status_check/stage.py b/astrbot/core/pipeline/session_status_check/stage.py deleted file mode 100644 index 26c3c235a3..0000000000 --- a/astrbot/core/pipeline/session_status_check/stage.py +++ /dev/null @@ -1,37 +0,0 @@ -from collections.abc import AsyncGenerator - -from astrbot.core import logger -from astrbot.core.platform.astr_message_event import AstrMessageEvent -from astrbot.core.star.session_llm_manager import SessionServiceManager - -from ..context import PipelineContext -from ..stage import Stage, register_stage - - -@register_stage -class SessionStatusCheckStage(Stage): - """检查会话是否整体启用""" - - async def initialize(self, ctx: PipelineContext) -> None: - self.ctx = ctx - self.conv_mgr = ctx.plugin_manager.context.conversation_manager - - async def process( - self, - event: AstrMessageEvent, - ) -> None | AsyncGenerator[None, None]: - # 检查会话是否整体启用 - if not await SessionServiceManager.is_session_enabled(event.unified_msg_origin): - logger.debug(f"会话 {event.unified_msg_origin} 已被关闭,已终止事件传播。") - - # workaround for #2309 - conv_id = await self.conv_mgr.get_curr_conversation_id( - event.unified_msg_origin, - ) - if not conv_id: - await self.conv_mgr.new_conversation( - event.unified_msg_origin, - platform_id=event.get_platform_id(), - ) - - event.stop_event() diff --git a/astrbot/core/pipeline/stage_order.py b/astrbot/core/pipeline/stage_order.py index f99f57264f..47a5c26ad7 100644 --- a/astrbot/core/pipeline/stage_order.py +++ b/astrbot/core/pipeline/stage_order.py @@ -3,7 +3,6 @@ STAGES_ORDER = [ "WakingCheckStage", # 检查是否需要唤醒 "WhitelistCheckStage", # 检查是否在群聊/私聊白名单 - "SessionStatusCheckStage", # 检查会话是否整体启用 "RateLimitStage", # 检查会话是否超过频率限制 "ContentSafetyCheckStage", # 检查内容安全 "PreProcessStage", # 预处理 diff --git a/astrbot/core/pipeline/waking_check/stage.py b/astrbot/core/pipeline/waking_check/stage.py index ddc2a6cb83..3b0547b88d 100644 --- a/astrbot/core/pipeline/waking_check/stage.py +++ b/astrbot/core/pipeline/waking_check/stage.py @@ -7,7 +7,6 @@ from astrbot.core.platform.message_type import MessageType from astrbot.core.star.filter.command_group import CommandGroupFilter from astrbot.core.star.filter.permission import PermissionTypeFilter -from astrbot.core.star.session_plugin_manager import SessionPluginManager from astrbot.core.star.star import star_map from astrbot.core.star.star_handler import EventType, star_handlers_registry @@ -51,56 +50,46 @@ async def initialize(self, ctx: PipelineContext) -> None: """ self.ctx = ctx - self.no_permission_reply = self.ctx.astrbot_config["platform_settings"].get( - "no_permission_reply", - True, - ) - # 私聊是否需要 wake_prefix 才能唤醒机器人 - self.friend_message_needs_wake_prefix = self.ctx.astrbot_config[ - "platform_settings" - ].get("friend_message_needs_wake_prefix", False) - # 是否忽略机器人自己发送的消息 - self.ignore_bot_self_message = self.ctx.astrbot_config["platform_settings"].get( - "ignore_bot_self_message", - False, - ) - self.ignore_at_all = self.ctx.astrbot_config["platform_settings"].get( - "ignore_at_all", - False, - ) - self.disable_builtin_commands = self.ctx.astrbot_config.get( - "disable_builtin_commands", False - ) - platform_settings = self.ctx.astrbot_config.get("platform_settings", {}) - self.unique_session = platform_settings.get("unique_session", False) async def process( self, event: AstrMessageEvent, ) -> None | AsyncGenerator[None, None]: + config = self.ctx.astrbot_config + platform_settings = config.get("platform_settings", {}) + no_permission_reply = platform_settings.get("no_permission_reply", True) + friend_message_needs_wake_prefix = platform_settings.get( + "friend_message_needs_wake_prefix", + False, + ) + ignore_bot_self_message = platform_settings.get( + "ignore_bot_self_message", + False, + ) + ignore_at_all = platform_settings.get("ignore_at_all", False) + disable_builtin_commands = config.get("disable_builtin_commands", False) + unique_session = platform_settings.get("unique_session", False) + # apply unique session - if self.unique_session and event.message_obj.type == MessageType.GROUP_MESSAGE: + if unique_session and event.message_obj.type == MessageType.GROUP_MESSAGE: sid = build_unique_session_id(event) if sid: event.session_id = sid # ignore bot self message - if ( - self.ignore_bot_self_message - and event.get_self_id() == event.get_sender_id() - ): + if ignore_bot_self_message and event.get_self_id() == event.get_sender_id(): event.stop_event() return # 设置 sender 身份 event.message_str = event.message_str.strip() - for admin_id in self.ctx.astrbot_config["admins_id"]: + for admin_id in config["admins_id"]: if str(event.get_sender_id()) == admin_id: event.role = "admin" break # 检查 wake - wake_prefixes = self.ctx.astrbot_config["wake_prefix"] + wake_prefixes = config["wake_prefix"] messages = event.get_messages() is_wake = False for wake_prefix in wake_prefixes: @@ -126,7 +115,7 @@ async def process( isinstance(message, At) and (str(message.qq) == str(event.get_self_id())) ) - or (isinstance(message, AtAll) and not self.ignore_at_all) + or (isinstance(message, AtAll) and not ignore_at_all) or ( isinstance(message, Reply) and str(message.sender_id) == str(event.get_self_id()) @@ -138,7 +127,7 @@ async def process( event.is_at_or_wake_command = True break # 检查是否是私聊 - if event.is_private_chat() and not self.friend_message_needs_wake_prefix: + if event.is_private_chat() and not friend_message_needs_wake_prefix: is_wake = True event.is_wake = True event.is_at_or_wake_command = True @@ -149,7 +138,8 @@ async def process( handlers_parsed_params = {} # 注册了指令的 handler # 将 plugins_name 设置到 event 中 - enabled_plugins_name = self.ctx.astrbot_config.get("plugin_set", ["*"]) + enabled_plugins_name = config.get("plugin_set", ["*"]) + disabled_plugins_name = set(config.get("plugin_disabled_set", [])) if enabled_plugins_name == ["*"]: # 如果是 *,则表示所有插件都启用 event.plugins_name = None @@ -162,11 +152,25 @@ async def process( plugins_name=event.plugins_name, ): if ( - self.disable_builtin_commands + disable_builtin_commands and handler.handler_module_path == "astrbot.builtin_stars.builtin_commands.main" ): continue + plugin = star_map.get(handler.handler_module_path) + if ( + plugin + and not plugin.reserved + and plugin.name + and plugin.name in disabled_plugins_name + ): + logger.debug( + "Plugin %s is disabled by config for session %s; skipping handler %s", + plugin.name, + event.unified_msg_origin, + handler.handler_name, + ) + continue # filter 需满足 AND 逻辑关系 passed = True @@ -178,10 +182,10 @@ async def process( for filter in handler.event_filters: try: if isinstance(filter, PermissionTypeFilter): - if not filter.filter(event, self.ctx.astrbot_config): + if not filter.filter(event, config): permission_not_pass = True permission_filter_raise_error = filter.raise_error - elif not filter.filter(event, self.ctx.astrbot_config): + elif not filter.filter(event, config): passed = False break except Exception as e: @@ -198,7 +202,7 @@ async def process( if not permission_filter_raise_error: # 跳过 continue - if self.no_permission_reply: + if no_permission_reply: await event.send( MessageChain().message( f"您(ID: {event.get_sender_id()})的权限不足以使用此指令。通过 /sid 获取 ID 并请管理员添加。", @@ -225,12 +229,6 @@ async def process( event._extras.pop("parsed_params", None) - # 根据会话配置过滤插件处理器 - activated_handlers = await SessionPluginManager.filter_handlers_by_session( - event, - activated_handlers, - ) - event.set_extra("activated_handlers", activated_handlers) event.set_extra("handlers_parsed_params", handlers_parsed_params) diff --git a/astrbot/core/pipeline/whitelist_check/stage.py b/astrbot/core/pipeline/whitelist_check/stage.py index ea9c55228e..efd70e26a5 100644 --- a/astrbot/core/pipeline/whitelist_check/stage.py +++ b/astrbot/core/pipeline/whitelist_check/stage.py @@ -13,30 +13,38 @@ class WhitelistCheckStage(Stage): """检查是否在群聊/私聊白名单""" async def initialize(self, ctx: PipelineContext) -> None: - self.enable_whitelist_check = ctx.astrbot_config["platform_settings"][ - "enable_id_white_list" - ] - self.whitelist = ctx.astrbot_config["platform_settings"]["id_whitelist"] - self.whitelist = [ - str(i).strip() for i in self.whitelist if str(i).strip() != "" - ] - self.wl_ignore_admin_on_group = ctx.astrbot_config["platform_settings"][ - "wl_ignore_admin_on_group" - ] - self.wl_ignore_admin_on_friend = ctx.astrbot_config["platform_settings"][ - "wl_ignore_admin_on_friend" - ] - self.wl_log = ctx.astrbot_config["platform_settings"]["id_whitelist_log"] + self.ctx = ctx async def process( self, event: AstrMessageEvent, ) -> None | AsyncGenerator[None, None]: - if not self.enable_whitelist_check: + platform_settings = self.ctx.astrbot_config["platform_settings"] + blacklist = [ + str(i).strip() + for i in platform_settings.get("id_blacklist", []) + if str(i).strip() + ] + event_group_id = str(event.get_group_id()).strip() + if event.unified_msg_origin in blacklist or event_group_id in blacklist: + logger.info( + "Session %s is in the session blacklist; stopping event propagation.", + event.unified_msg_origin, + ) + event.stop_event() + return + + enable_whitelist_check = platform_settings["enable_id_white_list"] + if not enable_whitelist_check: # 白名单检查未启用 return - if len(self.whitelist) == 0: + whitelist = [ + str(i).strip() + for i in platform_settings["id_whitelist"] + if str(i).strip() != "" + ] + if len(whitelist) == 0: # 白名单为空,不检查 return @@ -45,23 +53,23 @@ async def process( return # 检查是否在白名单 - if self.wl_ignore_admin_on_group: + if platform_settings["wl_ignore_admin_on_group"]: if ( event.role == "admin" and event.get_message_type() == MessageType.GROUP_MESSAGE ): return - if self.wl_ignore_admin_on_friend: + if platform_settings["wl_ignore_admin_on_friend"]: if ( event.role == "admin" and event.get_message_type() == MessageType.FRIEND_MESSAGE ): return if ( - event.unified_msg_origin not in self.whitelist - and str(event.get_group_id()).strip() not in self.whitelist + event.unified_msg_origin not in whitelist + and event_group_id not in whitelist ): - if self.wl_log: + if platform_settings["id_whitelist_log"]: logger.info( f"会话 ID {event.unified_msg_origin} 不在会话白名单中,已终止事件传播。请在配置文件中添加该会话 ID 到白名单。", ) diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index ae4001fcd6..f5e4695332 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -159,10 +159,14 @@ async def set_provider( if provider_id not in self.inst_map: raise ValueError(f"提供商 {provider_id} 不存在,无法设置。") if umo: - await sp.session_put( + provider_path_map = { + ProviderType.CHAT_COMPLETION: "provider_settings.default_provider_id", + ProviderType.SPEECH_TO_TEXT: "provider_stt_settings.provider_id", + ProviderType.TEXT_TO_SPEECH: "provider_tts_settings.provider_id", + } + await self.acm.update_conf_overrides( umo, - f"provider_perf_{provider_type.value}", - provider_id, + {provider_path_map[provider_type]: provider_id}, ) self._notify_provider_changed(provider_id, provider_type, umo) return @@ -225,18 +229,9 @@ def get_using_provider( """ provider = None provider_id = None - if umo: - provider_id = sp.get( - f"provider_perf_{provider_type.value}", - None, - scope="umo", - scope_id=umo, - ) - if provider_id: - provider = self.inst_map.get(provider_id) + config = self.acm.get_conf(umo) if not provider: # default setting - config = self.acm.get_conf(umo) if provider_type == ProviderType.CHAT_COMPLETION: provider_id = config["provider_settings"].get("default_provider_id") provider = self.inst_map.get(provider_id) diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index 11fc00eec4..2440f62769 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -498,9 +498,6 @@ def get_config(self, umo: str | None = None) -> AstrBotConfig: Note: 如果不提供 umo 参数,将返回默认配置。 """ - if not umo: - # 使用默认配置 - return self._config return self.astrbot_config_mgr.get_conf(umo) async def send_message( diff --git a/astrbot/core/star/session_llm_manager.py b/astrbot/core/star/session_llm_manager.py deleted file mode 100644 index ad4a473b47..0000000000 --- a/astrbot/core/star/session_llm_manager.py +++ /dev/null @@ -1,185 +0,0 @@ -"""会话服务管理器 - 负责管理每个会话的LLM、TTS等服务的启停状态""" - -from astrbot.core import logger, sp -from astrbot.core.platform.astr_message_event import AstrMessageEvent - - -class SessionServiceManager: - """管理会话级别的服务启停状态,包括LLM和TTS""" - - # ============================================================================= - # LLM 相关方法 - # ============================================================================= - - @staticmethod - async def is_llm_enabled_for_session(session_id: str) -> bool: - """检查LLM是否在指定会话中启用 - - Args: - session_id: 会话ID (unified_msg_origin) - - Returns: - bool: True表示启用,False表示禁用 - - """ - # 获取会话服务配置 - session_services = await sp.get_async( - scope="umo", - scope_id=session_id, - key="session_service_config", - default={}, - ) - - # 如果配置了该会话的LLM状态,返回该状态 - llm_enabled = session_services.get("llm_enabled") - if llm_enabled is not None: - return llm_enabled - - # 如果没有配置,默认为启用(兼容性考虑) - return True - - @staticmethod - async def set_llm_status_for_session(session_id: str, enabled: bool) -> None: - """设置LLM在指定会话中的启停状态 - - Args: - session_id: 会话ID (unified_msg_origin) - enabled: True表示启用,False表示禁用 - - """ - session_config = ( - await sp.get_async( - scope="umo", - scope_id=session_id, - key="session_service_config", - default={}, - ) - or {} - ) - session_config["llm_enabled"] = enabled - await sp.put_async( - scope="umo", - scope_id=session_id, - key="session_service_config", - value=session_config, - ) - - @staticmethod - async def should_process_llm_request(event: AstrMessageEvent) -> bool: - """检查是否应该处理LLM请求 - - Args: - event: 消息事件 - - Returns: - bool: True表示应该处理,False表示跳过 - - """ - session_id = event.unified_msg_origin - return await SessionServiceManager.is_llm_enabled_for_session(session_id) - - # ============================================================================= - # TTS 相关方法 - # ============================================================================= - - @staticmethod - async def is_tts_enabled_for_session(session_id: str) -> bool: - """检查TTS是否在指定会话中启用 - - Args: - session_id: 会话ID (unified_msg_origin) - - Returns: - bool: True表示启用,False表示禁用 - - """ - # 获取会话服务配置 - session_services = await sp.get_async( - scope="umo", - scope_id=session_id, - key="session_service_config", - default={}, - ) - - # 如果配置了该会话的TTS状态,返回该状态 - tts_enabled = session_services.get("tts_enabled") - if tts_enabled is not None: - return tts_enabled - - # 如果没有配置,默认为启用(兼容性考虑) - return True - - @staticmethod - async def set_tts_status_for_session(session_id: str, enabled: bool) -> None: - """设置TTS在指定会话中的启停状态 - - Args: - session_id: 会话ID (unified_msg_origin) - enabled: True表示启用,False表示禁用 - - """ - session_config = ( - await sp.get_async( - scope="umo", - scope_id=session_id, - key="session_service_config", - default={}, - ) - or {} - ) - session_config["tts_enabled"] = enabled - await sp.put_async( - scope="umo", - scope_id=session_id, - key="session_service_config", - value=session_config, - ) - - logger.info( - f"会话 {session_id} 的TTS状态已更新为: {'启用' if enabled else '禁用'}", - ) - - @staticmethod - async def should_process_tts_request(event: AstrMessageEvent) -> bool: - """检查是否应该处理TTS请求 - - Args: - event: 消息事件 - - Returns: - bool: True表示应该处理,False表示跳过 - - """ - session_id = event.unified_msg_origin - return await SessionServiceManager.is_tts_enabled_for_session(session_id) - - # ============================================================================= - # 会话整体启停相关方法 - # ============================================================================= - - @staticmethod - async def is_session_enabled(session_id: str) -> bool: - """检查会话是否整体启用 - - Args: - session_id: 会话ID (unified_msg_origin) - - Returns: - bool: True表示启用,False表示禁用 - - """ - # 获取会话服务配置 - session_services = await sp.get_async( - scope="umo", - scope_id=session_id, - key="session_service_config", - default={}, - ) - - # 如果配置了该会话的整体状态,返回该状态 - session_enabled = session_services.get("session_enabled") - if session_enabled is not None: - return session_enabled - - # 如果没有配置,默认为启用(兼容性考虑) - return True diff --git a/astrbot/core/star/session_plugin_manager.py b/astrbot/core/star/session_plugin_manager.py deleted file mode 100644 index a81113415b..0000000000 --- a/astrbot/core/star/session_plugin_manager.py +++ /dev/null @@ -1,101 +0,0 @@ -"""会话插件管理器 - 负责管理每个会话的插件启停状态""" - -from astrbot.core import logger, sp -from astrbot.core.platform.astr_message_event import AstrMessageEvent - - -class SessionPluginManager: - """管理会话级别的插件启停状态""" - - @staticmethod - async def is_plugin_enabled_for_session( - session_id: str, - plugin_name: str, - ) -> bool: - """检查插件是否在指定会话中启用 - - Args: - session_id: 会话ID (unified_msg_origin) - plugin_name: 插件名称 - - Returns: - bool: True表示启用,False表示禁用 - - """ - # 获取会话插件配置 - session_plugin_config = await sp.get_async( - scope="umo", - scope_id=session_id, - key="session_plugin_config", - default={}, - ) - session_config = session_plugin_config.get(session_id, {}) - - enabled_plugins = session_config.get("enabled_plugins", []) - disabled_plugins = session_config.get("disabled_plugins", []) - - # 如果插件在禁用列表中,返回False - if plugin_name in disabled_plugins: - return False - - # 如果插件在启用列表中,返回True - if plugin_name in enabled_plugins: - return True - - # 如果都没有配置,默认为启用(兼容性考虑) - return True - - @staticmethod - async def filter_handlers_by_session( - event: AstrMessageEvent, - handlers: list, - ) -> list: - """根据会话配置过滤处理器列表 - - Args: - event: 消息事件 - handlers: 原始处理器列表 - - Returns: - List: 过滤后的处理器列表 - - """ - from astrbot.core.star.star import star_map - - session_id = event.unified_msg_origin - filtered_handlers = [] - - session_plugin_config = await sp.get_async( - scope="umo", - scope_id=session_id, - key="session_plugin_config", - default={}, - ) - session_config = session_plugin_config.get(session_id, {}) - disabled_plugins = session_config.get("disabled_plugins", []) - - for handler in handlers: - # 获取处理器对应的插件 - plugin = star_map.get(handler.handler_module_path) - if not plugin: - # 如果找不到插件元数据,允许执行(可能是系统插件) - filtered_handlers.append(handler) - continue - - # 跳过保留插件(系统插件) - if plugin.reserved: - filtered_handlers.append(handler) - continue - - if plugin.name is None: - continue - - # 检查插件是否在当前会话中启用 - if plugin.name in disabled_plugins: - logger.debug( - f"插件 {plugin.name} 在会话 {session_id} 中被禁用,跳过处理器 {handler.handler_name}", - ) - else: - filtered_handlers.append(handler) - - return filtered_handlers diff --git a/astrbot/core/tools/knowledge_base_tools.py b/astrbot/core/tools/knowledge_base_tools.py index e082fd4253..8d630da2b8 100644 --- a/astrbot/core/tools/knowledge_base_tools.py +++ b/astrbot/core/tools/knowledge_base_tools.py @@ -1,7 +1,7 @@ from pydantic import Field from pydantic.dataclasses import dataclass -from astrbot.api import logger, sp +from astrbot.api import logger from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.agent.tool import FunctionTool, ToolExecResult from astrbot.core.astr_agent_context import AstrAgentContext @@ -30,41 +30,21 @@ async def retrieve_knowledge_base( kb_mgr = context.kb_manager config = context.get_config(umo=umo) - session_config = await sp.session_get(umo, "kb_config", default={}) - if session_config and "kb_ids" in session_config: - kb_ids = session_config.get("kb_ids", []) - if not kb_ids: - logger.info(f"[知识库] 会话 {umo} 已被配置为不使用知识库") - return None - - top_k = session_config.get("top_k", 5) - kb_names = [] - invalid_kb_ids = [] - for kb_id in kb_ids: - kb_helper = await kb_mgr.get_kb(kb_id) - if kb_helper: - kb_names.append(kb_helper.kb.kb_name) - else: - logger.warning(f"[知识库] 知识库不存在或未加载: {kb_id}") - invalid_kb_ids.append(kb_id) - - if invalid_kb_ids: - logger.warning( - f"[知识库] 会话 {umo} 配置的以下知识库无效: {invalid_kb_ids}", - ) - if not kb_names: - return None - logger.debug(f"[知识库] 使用会话级配置,知识库数量: {len(kb_names)}") - else: - kb_names = config.get("kb_names", []) - top_k = config.get("kb_final_top_k", 5) - logger.debug(f"[知识库] 使用全局配置,知识库数量: {len(kb_names)}") + kb_names = config.get("kb_names", []) + top_k = config.get("kb_final_top_k", 5) + logger.debug(f"[知识库] 使用配置文件,知识库数量: {len(kb_names)}") top_k_fusion = config.get("kb_fusion_top_k", 20) if not kb_names: return None - all_kbs = [await kb_mgr.get_kb_by_name(kb) for kb in kb_names] + all_kbs = [] + for kb_ref in kb_names: + kb_ref = str(kb_ref) + kb_helper = await kb_mgr.get_kb(kb_ref) + if not kb_helper: + kb_helper = await kb_mgr.get_kb_by_name(kb_ref) + all_kbs.append(kb_helper) if check_all_kb(all_kbs): logger.debug("所配置的所有知识库全为空,跳过检索过程") return None diff --git a/astrbot/core/utils/migra_helper.py b/astrbot/core/utils/migra_helper.py index 40b899620d..186f3432df 100644 --- a/astrbot/core/utils/migra_helper.py +++ b/astrbot/core/utils/migra_helper.py @@ -1,6 +1,6 @@ import traceback -from astrbot.core import astrbot_config, logger +from astrbot.core import astrbot_config, logger, sp from astrbot.core.agent.runners.deerflow.constants import ( DEERFLOW_AGENT_RUNNER_PROVIDER_ID_KEY, DEERFLOW_PROVIDER_TYPE, @@ -9,6 +9,9 @@ from astrbot.core.db.migration.migra_45_to_46 import migrate_45_to_46 from astrbot.core.db.migration.migra_token_usage import migrate_token_usage from astrbot.core.db.migration.migra_webchat_session import migrate_webchat_session +from astrbot.core.provider.entities import ProviderType + +CONFIG_OVERRIDE_MIGRATION_DONE_KEY = "migration_config_overrides_v1_done" def _migra_agent_runner_configs(conf: AstrBotConfig, ids_map: dict) -> None: @@ -128,6 +131,98 @@ def _migra_provider_to_source_structure(conf: AstrBotConfig) -> None: logger.info("Provider-source structure migration completed") +async def _migrate_session_rules_to_config_overrides( + db, + acm: AstrBotConfigManager, +) -> None: + """Migrate legacy UMO session rules into config override preferences. + + Args: + db: Database helper used to migrate custom UMO names into aliases. + acm: Config manager used to write override paths. + """ + migration_done = await sp.global_get(CONFIG_OVERRIDE_MIGRATION_DONE_KEY, False) + if migration_done: + return + + provider_path_map = { + f"provider_perf_{ProviderType.CHAT_COMPLETION.value}": ( + "provider_settings.default_provider_id" + ), + f"provider_perf_{ProviderType.SPEECH_TO_TEXT.value}": ( + "provider_stt_settings.provider_id" + ), + f"provider_perf_{ProviderType.TEXT_TO_SPEECH.value}": ( + "provider_tts_settings.provider_id" + ), + } + legacy_keys = { + "session_service_config", + "session_plugin_config", + "kb_config", + *provider_path_map.keys(), + } + prefs = await sp.session_get(None, None) + migrated_count = 0 + for pref in prefs: + if pref.key not in legacy_keys: + continue + umo = pref.scope_id + value = pref.value.get("val") if isinstance(pref.value, dict) else None + override_paths = {} + + if pref.key == "session_service_config" and isinstance(value, dict): + if "llm_enabled" in value: + override_paths["provider_settings.enable"] = bool(value["llm_enabled"]) + if "tts_enabled" in value: + override_paths["provider_tts_settings.enable"] = bool( + value["tts_enabled"] + ) + if "persona_id" in value: + override_paths["provider_settings.default_personality"] = ( + value.get("persona_id") or "" + ) + if value.get("session_enabled") is False: + override_paths["platform_settings.id_blacklist"] = [umo] + custom_name = str(value.get("custom_name") or "").strip() + if custom_name: + alias = await db.get_umo_alias(umo) + if not alias or not alias.user_alias: + await db.upsert_umo_alias( + umo, + alias.creator_sender_id if alias else "", + alias.auto_name if alias else None, + custom_name, + ) + elif pref.key == "session_plugin_config" and isinstance(value, dict): + session_config = value.get(umo, value) + if isinstance(session_config, dict): + disabled_plugins = session_config.get("disabled_plugins") + if isinstance(disabled_plugins, list): + override_paths["plugin_disabled_set"] = disabled_plugins + elif pref.key == "kb_config" and isinstance(value, dict): + if "top_k" in value: + override_paths["kb_final_top_k"] = value["top_k"] + kb_ids = value.get("kb_ids") + if isinstance(kb_ids, list): + override_paths["kb_names"] = [ + str(kb_id) for kb_id in kb_ids if str(kb_id).strip() + ] + elif pref.key in provider_path_map and value: + override_paths[provider_path_map[pref.key]] = value + + if override_paths: + await acm.update_conf_overrides(umo, override_paths) + migrated_count += 1 + + await sp.global_put(CONFIG_OVERRIDE_MIGRATION_DONE_KEY, True) + if migrated_count: + logger.info( + "Migrated %s legacy session rule preferences to config overrides.", + migrated_count, + ) + + async def migra( db, astrbot_config_mgr, umop_config_router, acm: AstrBotConfigManager ) -> None: @@ -156,6 +251,12 @@ async def migra( logger.error(f"Migration for token_usage column failed: {e!s}") logger.error(traceback.format_exc()) + try: + await _migrate_session_rules_to_config_overrides(db, acm) + except Exception as e: + logger.error(f"Migration for session rule config overrides failed: {e!s}") + logger.error(traceback.format_exc()) + # migra third party agent runner configs _c = False providers = astrbot_config["provider"] diff --git a/astrbot/dashboard/api/conversations.py b/astrbot/dashboard/api/conversations.py index 50edc5fc6b..3c837505f8 100644 --- a/astrbot/dashboard/api/conversations.py +++ b/astrbot/dashboard/api/conversations.py @@ -93,6 +93,7 @@ async def _list_conversations( platforms: str, message_types: str, search: str, + user_id: str, exclude_ids: str, exclude_platforms: str, ): @@ -103,6 +104,7 @@ async def _list_conversations( platforms=platforms, message_types=message_types, search_query=search, + user_id=user_id, exclude_ids=exclude_ids, exclude_platforms=exclude_platforms, ) @@ -116,6 +118,7 @@ async def list_conversations( platforms: str = Query(default=""), message_types: str = Query(default=""), search: str = Query(default=""), + user_id: str = Query(default=""), exclude_ids: str = Query(default=""), exclude_platforms: str = Query(default=""), _auth: AuthContext = Depends(require_data_scope), @@ -128,6 +131,7 @@ async def list_conversations( platforms=platforms, message_types=message_types, search=search, + user_id=user_id, exclude_ids=exclude_ids, exclude_platforms=exclude_platforms, ) @@ -222,6 +226,7 @@ async def list_dashboard_conversations( platforms: str = Query(default=""), message_types: str = Query(default=""), search: str = Query(default=""), + user_id: str = Query(default=""), exclude_ids: str = Query(default=""), exclude_platforms: str = Query(default=""), _username: str = Depends(require_dashboard_user), @@ -234,6 +239,7 @@ async def list_dashboard_conversations( platforms=platforms, message_types=message_types, search=search, + user_id=user_id, exclude_ids=exclude_ids, exclude_platforms=exclude_platforms, ) diff --git a/astrbot/dashboard/api/router.py b/astrbot/dashboard/api/router.py index b59ea62858..e83b360836 100644 --- a/astrbot/dashboard/api/router.py +++ b/astrbot/dashboard/api/router.py @@ -28,6 +28,7 @@ from .t2i import router as t2i_router from .tools import router as tools_router from .updates import router as updates_router +from .workspaces import router as workspaces_router API_V1_PREFIX = "/api/v1" @@ -58,6 +59,7 @@ def build_api_router() -> APIRouter: router.include_router(t2i_router) router.include_router(personas_router) router.include_router(updates_router) + router.include_router(workspaces_router) router.include_router(open_api_router) router.include_router(live_chat_router) return router diff --git a/astrbot/dashboard/api/sessions.py b/astrbot/dashboard/api/sessions.py index 42ede8ea5e..bd47a45a89 100644 --- a/astrbot/dashboard/api/sessions.py +++ b/astrbot/dashboard/api/sessions.py @@ -8,6 +8,9 @@ from astrbot.dashboard.schemas import ( BatchSessionProviderRequest, BatchSessionServiceRequest, + SessionAliasRequest, + SessionConfigOverrideDeleteRequest, + SessionConfigOverrideRequest, SessionGroupRequest, SessionRuleRequest, UmoListRequest, @@ -165,6 +168,82 @@ async def delete_session_rule( return _unexpected_error("删除会话规则失败", exc) +@router.get("/sessions/config-overrides") +async def list_session_config_overrides( + page: int = Query(1), + page_size: int = Query(10), + search: str = Query(""), + umo: str = Query(""), + _auth: AuthContext = Depends(require_data_scope), + service: SessionManagementService = Depends(get_service), +): + try: + return ok( + await service.list_session_config_overrides( + page=page, + page_size=page_size, + search=search.strip(), + umo=umo.strip(), + ) + ) + except SessionManagementServiceError as exc: + return _service_error(exc) + except Exception as exc: + return _unexpected_error("获取会话配置覆盖项列表失败", exc) + + +@router.post("/sessions/config-overrides") +async def upsert_session_config_override( + payload: SessionConfigOverrideRequest, + _auth: AuthContext = Depends(require_data_scope), + service: SessionManagementService = Depends(get_service), +): + try: + return ok( + await service.update_session_config_override( + payload.model_dump(exclude_unset=True) + ) + ) + except SessionManagementServiceError as exc: + return _service_error(exc) + except Exception as exc: + return _unexpected_error("更新会话配置覆盖项失败", exc) + + +@router.post("/sessions/config-overrides/delete") +async def delete_session_config_override( + payload: SessionConfigOverrideDeleteRequest, + _auth: AuthContext = Depends(require_data_scope), + service: SessionManagementService = Depends(get_service), +): + try: + return ok( + await service.delete_session_config_override( + payload.model_dump(exclude_none=True) + ) + ) + except SessionManagementServiceError as exc: + return _service_error(exc) + except Exception as exc: + return _unexpected_error("删除会话配置覆盖项失败", exc) + + +@router.post("/sessions/aliases") +async def upsert_session_alias( + payload: SessionAliasRequest, + _auth: AuthContext = Depends(require_data_scope), + service: SessionManagementService = Depends(get_service), +): + try: + return ok( + await service.update_session_alias(payload.model_dump(exclude_unset=True)) + ) + except SessionManagementServiceError as exc: + return _service_error(exc) + except Exception as exc: + return _unexpected_error("更新会话备注失败", exc) + + @router.patch("/sessions/provider") async def update_session_provider( payload: BatchSessionProviderRequest, diff --git a/astrbot/dashboard/api/workspaces.py b/astrbot/dashboard/api/workspaces.py new file mode 100644 index 0000000000..be6d9ac559 --- /dev/null +++ b/astrbot/dashboard/api/workspaces.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from pathlib import Path + +from fastapi import APIRouter, Depends, Query, Request + +from astrbot.core.utils.datetime_utils import to_utc_isoformat +from astrbot.core.workspace import resolve_workspace_root_for_umo +from astrbot.dashboard.responses import ApiError, ok + +from .auth import AuthContext, require_scope + +router = APIRouter(tags=["Workspaces"]) + + +async def require_data_scope(request: Request) -> AuthContext: + return await require_scope(request, "data") + + +def _format_mtime(path: Path) -> str | None: + """Format a filesystem modified timestamp for API responses. + + Args: + path: File or directory path. + + Returns: + UTC ISO timestamp, or None when the timestamp cannot be read. + """ + try: + return to_utc_isoformat( + datetime.fromtimestamp(path.stat().st_mtime, timezone.utc) + ) + except OSError: + return None + + +@router.get("/workspaces/by-umo") +async def list_umo_workspace_files( + request: Request, + umo: str = Query(..., min_length=1), + path: str | None = Query(default=None), + _auth: AuthContext = Depends(require_data_scope), +): + """List files in the workspace resolved for one UMO. + + Args: + request: FastAPI request with database state. + umo: Unified message origin. + path: Relative directory path under the workspace root. + _auth: Auth context. + + Returns: + Workspace metadata and one directory level of file entries. + + Raises: + ApiError: If the requested path escapes the workspace root. + """ + root = await resolve_workspace_root_for_umo(umo, request.app.state.db) + root = root.expanduser().resolve(strict=False) + raw_path = str(path or "").strip() + requested = root if raw_path in {"", "."} else root / raw_path + current = requested.resolve(strict=False) + + if current != root and not current.is_relative_to(root): + raise ApiError("Path is outside the workspace", status_code=400) + + if not root.exists() or not root.is_dir(): + return ok( + { + "umo": umo, + "absolute_path": str(root), + "relative_path": "", + "exists": False, + "is_directory": False, + "items": [], + } + ) + if not current.exists() or not current.is_dir(): + raise ApiError("Workspace folder not found", status_code=404) + + items = [] + try: + children = sorted( + current.iterdir(), + key=lambda item: ( + not (item.is_dir() and not item.is_symlink()), + item.name.lower(), + ), + ) + except OSError as exc: + raise ApiError(f"Failed to list workspace files: {exc!s}") from exc + + for child in children: + try: + is_symlink = child.is_symlink() + stat_result = child.lstat() if is_symlink else child.stat() + if is_symlink: + item_type = "symlink" + elif child.is_dir(): + item_type = "directory" + elif child.is_file(): + item_type = "file" + else: + item_type = "other" + items.append( + { + "name": child.name, + "type": item_type, + "relative_path": child.relative_to(root).as_posix(), + "size_bytes": 0 + if item_type == "directory" + else stat_result.st_size, + "modified_at": _format_mtime(child), + } + ) + except OSError: + continue + + return ok( + { + "umo": umo, + "absolute_path": str(root), + "relative_path": "" + if current == root + else current.relative_to(root).as_posix(), + "exists": True, + "is_directory": True, + "items": items, + } + ) diff --git a/astrbot/dashboard/schemas.py b/astrbot/dashboard/schemas.py index 37d940a71a..ca16b5804a 100644 --- a/astrbot/dashboard/schemas.py +++ b/astrbot/dashboard/schemas.py @@ -613,6 +613,29 @@ class SessionRuleRequest(OpenModel): rule_value: Any = None +class SessionConfigOverrideRequest(OpenModel): + umo: str | None = None + path: str | None = None + value: Any = None + + +class SessionConfigOverrideDeleteRequest(OpenModel): + umo: str | None = None + umos: list[str] | None = None + scope: Literal["all", "group", "private", "custom_group"] | None = None + group_id: str | None = None + path: str | None = None + paths: list[str] | None = None + + +class SessionAliasRequest(OpenModel): + umo: str | None = None + umos: list[str] | None = None + scope: Literal["all", "group", "private", "custom_group"] | None = None + group_id: str | None = None + custom_name: str | None = None + + class UmoListRequest(OpenModel): umo: str | None = None umos: list[str] | None = None diff --git a/astrbot/dashboard/services/conversation_service.py b/astrbot/dashboard/services/conversation_service.py index b73b12098f..9a3a722970 100644 --- a/astrbot/dashboard/services/conversation_service.py +++ b/astrbot/dashboard/services/conversation_service.py @@ -40,6 +40,7 @@ async def list_conversations( platforms: str, message_types: str, search_query: str, + user_id: str, exclude_ids: str, exclude_platforms: str, ) -> dict: @@ -62,6 +63,7 @@ async def list_conversations( platforms=platform_list, message_types=message_type_list, search_query=search_query, + user_id=user_id, exclude_ids=exclude_id_list, exclude_platforms=exclude_platform_list, ) diff --git a/astrbot/dashboard/services/session_management_service.py b/astrbot/dashboard/services/session_management_service.py index a66f4e8e97..784a3eeb8e 100644 --- a/astrbot/dashboard/services/session_management_service.py +++ b/astrbot/dashboard/services/session_management_service.py @@ -110,27 +110,87 @@ async def get_umo_rules( page: int = 1, page_size: int = 10, search: str = "", + umo: str = "", ) -> tuple[dict, int]: umo_rules = {} + config_mgr = self.core_lifecycle.astrbot_config_mgr async with self.db_helper.get_db() as session: session: AsyncSession result = await session.execute( select(Preference).where( col(Preference.scope) == "umo", - col(Preference.key).in_(AVAILABLE_SESSION_RULE_KEYS), + col(Preference.key) == config_mgr.core_config_override_key, ) ) prefs = result.scalars().all() for pref in prefs: umo_id = pref.scope_id - if umo_id not in umo_rules: - umo_rules[umo_id] = {} - if pref.key == "session_plugin_config" and umo_id in pref.value["val"]: - umo_rules[umo_id][pref.key] = pref.value["val"][umo_id] - else: - umo_rules[umo_id][pref.key] = pref.value["val"] + paths = config_mgr.normalize_conf_override_payload( + pref.value.get("val") if isinstance(pref.value, dict) else None + ) + rules = {} + service_config = {} + if "provider_settings.enable" in paths: + service_config["llm_enabled"] = bool( + paths["provider_settings.enable"] + ) + if "provider_tts_settings.enable" in paths: + service_config["tts_enabled"] = bool( + paths["provider_tts_settings.enable"] + ) + if "provider_settings.default_personality" in paths: + service_config["persona_id"] = paths[ + "provider_settings.default_personality" + ] + if "platform_settings.id_blacklist" in paths: + blacklist = [ + str(item).strip() + for item in paths["platform_settings.id_blacklist"] + if str(item).strip() + ] + service_config["session_enabled"] = umo_id not in blacklist + if service_config: + rules["session_service_config"] = service_config + + if isinstance(paths.get("plugin_disabled_set"), list): + rules["session_plugin_config"] = { + "disabled_plugins": paths["plugin_disabled_set"] + } - alias_map = await self.get_umo_alias_map(list(umo_rules.keys())) + kb_config = {} + if "kb_names" in paths: + kb_config["kb_names"] = paths["kb_names"] + if "kb_final_top_k" in paths: + kb_config["top_k"] = paths["kb_final_top_k"] + if kb_config: + rules["kb_config"] = kb_config + + provider_rule_map = { + "provider_settings.default_provider_id": ( + f"provider_perf_{ProviderType.CHAT_COMPLETION.value}" + ), + "provider_stt_settings.provider_id": ( + f"provider_perf_{ProviderType.SPEECH_TO_TEXT.value}" + ), + "provider_tts_settings.provider_id": ( + f"provider_perf_{ProviderType.TEXT_TO_SPEECH.value}" + ), + } + for config_path, rule_key in provider_rule_map.items(): + if config_path in paths: + rules[rule_key] = paths[config_path] + + if rules: + umo_rules[umo_id] = rules + + aliases = await self.db_helper.get_umo_aliases() + alias_map = build_umo_alias_map(aliases) + for alias in aliases: + alias_info = serialize_umo_alias(alias, alias.umo) + if alias_info.get("user_alias"): + umo_rules.setdefault(alias.umo, {}).setdefault( + "session_service_config", {} + )["custom_name"] = alias_info["user_alias"] if search: search_lower = search.lower() @@ -155,6 +215,11 @@ async def get_umo_rules( filtered_rules[umo_id] = rules umo_rules = filtered_rules + if umo: + umo_rules = { + umo_id: rules for umo_id, rules in umo_rules.items() if umo_id == umo + } + total = len(umo_rules) all_umo_ids = list(umo_rules.keys()) start_idx = (page - 1) * page_size @@ -169,12 +234,15 @@ async def list_session_rules( page: int, page_size: int, search: str, + include_available_options: bool = True, + umo: str = "", ) -> dict: page, page_size = self._normalize_page(page, page_size, default_page_size=10) umo_rules, total = await self.get_umo_rules( page=page, page_size=page_size, search=search, + umo=umo, ) alias_map = await self.get_umo_alias_map(list(umo_rules.keys())) @@ -186,6 +254,15 @@ async def list_session_rules( for umo, rules in umo_rules.items() ] + result = { + "rules": rules_list, + "total": total, + "page": page, + "page_size": page_size, + } + if not include_available_options: + return result + provider_manager = self.core_lifecycle.provider_manager persona_mgr = getattr(self.core_lifecycle, "persona_mgr", None) plugin_manager = getattr(self.core_lifecycle, "plugin_manager", None) @@ -222,24 +299,229 @@ async def list_session_rules( except Exception as exc: logger.warning(f"获取知识库列表失败: {exc!s}") + kb_name_to_id = { + kb["kb_name"]: kb["kb_id"] + for kb in available_kbs + if kb.get("kb_name") and kb.get("kb_id") + } + kb_id_set = {kb["kb_id"] for kb in available_kbs if kb.get("kb_id")} + for item in rules_list: + kb_config = item.get("rules", {}).get("kb_config") + if isinstance(kb_config, dict) and "kb_names" in kb_config: + kb_config["kb_ids"] = [ + str(kb_ref) + if str(kb_ref) in kb_id_set + else kb_name_to_id[str(kb_ref)] + for kb_ref in kb_config["kb_names"] + if str(kb_ref) in kb_id_set or str(kb_ref) in kb_name_to_id + ] + + result.update( + { + "available_personas": available_personas, + "available_chat_providers": self._serialize_provider_insts( + getattr(provider_manager, "provider_insts", []) + ), + "available_stt_providers": self._serialize_provider_insts( + getattr(provider_manager, "stt_provider_insts", []) + ), + "available_tts_providers": self._serialize_provider_insts( + getattr(provider_manager, "tts_provider_insts", []) + ), + "available_plugins": available_plugins, + "available_kbs": available_kbs, + "available_rule_keys": AVAILABLE_SESSION_RULE_KEYS, + } + ) + return result + + async def list_session_config_overrides( + self, + *, + page: int, + page_size: int, + search: str, + umo: str = "", + ) -> dict: + """List UMO config overrides without editor option payloads. + + Args: + page: Page number. + page_size: Number of items per page. + search: Search keyword. + + Returns: + Paginated config override rows. + """ + return await self.list_session_rules( + page=page, + page_size=page_size, + search=search, + umo=umo, + include_available_options=False, + ) + + async def update_session_config_override(self, data: object) -> dict: + """Update one core config override path for a UMO. + + Args: + data: Request payload containing umo, path, and value. + + Returns: + Operation result with the updated UMO and config path. + + Raises: + SessionManagementServiceError: If required payload fields are missing. + """ + payload = self._payload(data) + umo = str(payload.get("umo") or "").strip() + path = str(payload.get("path") or "").strip() + + if not umo: + raise SessionManagementServiceError("缺少必要参数: umo") + if not path: + raise SessionManagementServiceError("缺少必要参数: path") + if "value" not in payload: + raise SessionManagementServiceError("缺少必要参数: value") + + await self.core_lifecycle.astrbot_config_mgr.update_conf_overrides( + umo, + {path: payload.get("value")}, + ) + return {"message": f"配置覆盖项 {path} 已更新", "umo": umo, "path": path} + + async def delete_session_config_override(self, data: object) -> dict: + """Delete one or more core config override paths for a UMO. + + Args: + data: Request payload containing UMO selection and path or paths. + + Returns: + Operation result with updated UMOs and removed paths. + + Raises: + SessionManagementServiceError: If required payload fields are missing. + """ + payload = self._payload(data) + umos = [ + str(umo).strip() + for umo in ( + payload.get("umos") if isinstance(payload.get("umos"), list) else [] + ) + if str(umo).strip() + ] + umo = str(payload.get("umo") or "").strip() + if umo: + umos.append(umo) + + scope = str(payload.get("scope") or "").strip() + group_id = str(payload.get("group_id") or "").strip() + if scope and not umos: + umos = await self.get_umos_by_scope(scope, group_id) + umos = list(dict.fromkeys(umos)) + + raw_paths = payload.get("paths") + paths = [ + str(path).strip() + for path in (raw_paths if isinstance(raw_paths, list) else []) + if str(path).strip() + ] + path = str(payload.get("path") or "").strip() + if path: + paths.append(path) + paths = list(dict.fromkeys(paths)) + + if not umos: + raise SessionManagementServiceError( + "缺少必要参数: umo、umos 或有效的 scope" + ) + if not paths: + raise SessionManagementServiceError("缺少必要参数: path 或 paths") + + success_count = 0 + failed_umos = [] + for target_umo in umos: + try: + await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides( + target_umo, + paths, + ) + success_count += 1 + except Exception as exc: + logger.error( + f"Failed to delete config override for {target_umo}: {exc!s}" + ) + failed_umos.append(target_umo) + return { - "rules": rules_list, - "total": total, - "page": page, - "page_size": page_size, - "available_personas": available_personas, - "available_chat_providers": self._serialize_provider_insts( - getattr(provider_manager, "provider_insts", []) - ), - "available_stt_providers": self._serialize_provider_insts( - getattr(provider_manager, "stt_provider_insts", []) - ), - "available_tts_providers": self._serialize_provider_insts( - getattr(provider_manager, "tts_provider_insts", []) - ), - "available_plugins": available_plugins, - "available_kbs": available_kbs, - "available_rule_keys": AVAILABLE_SESSION_RULE_KEYS, + "message": f"已删除 {success_count} 个会话的配置覆盖项", + "success_count": success_count, + "failed_count": len(failed_umos), + "failed_umos": failed_umos, + "paths": paths, + } + + async def update_session_alias(self, data: object) -> dict: + """Update UMO user aliases used as session remarks. + + Args: + data: Request payload containing UMO selection and custom_name. + + Returns: + Operation result with updated UMO count. + + Raises: + SessionManagementServiceError: If required payload fields are missing. + """ + payload = self._payload(data) + if "custom_name" not in payload: + raise SessionManagementServiceError("缺少必要参数: custom_name") + + umos = [ + str(umo).strip() + for umo in ( + payload.get("umos") if isinstance(payload.get("umos"), list) else [] + ) + if str(umo).strip() + ] + umo = str(payload.get("umo") or "").strip() + if umo: + umos.append(umo) + + scope = str(payload.get("scope") or "").strip() + group_id = str(payload.get("group_id") or "").strip() + if scope and not umos: + umos = await self.get_umos_by_scope(scope, group_id) + umos = list(dict.fromkeys(umos)) + + if not umos: + raise SessionManagementServiceError( + "缺少必要参数: umo、umos 或有效的 scope" + ) + + custom_name = str(payload.get("custom_name") or "").strip() + success_count = 0 + failed_umos = [] + for target_umo in umos: + try: + alias = await self.db_helper.get_umo_alias(target_umo) + if alias or custom_name: + await self.db_helper.upsert_umo_alias( + target_umo, + alias.creator_sender_id if alias else "", + alias.auto_name if alias else None, + custom_name or None, + ) + success_count += 1 + except Exception as exc: + logger.error(f"Failed to update alias for {target_umo}: {exc!s}") + failed_umos.append(target_umo) + + return { + "message": f"已更新 {success_count} 个会话备注", + "success_count": success_count, + "failed_count": len(failed_umos), + "failed_umos": failed_umos, } async def update_session_rule(self, data: object) -> dict: @@ -255,10 +537,72 @@ async def update_session_rule(self, data: object) -> dict: if rule_key not in AVAILABLE_SESSION_RULE_KEYS: raise SessionManagementServiceError(f"不支持的规则键: {rule_key}") - if rule_key == "session_plugin_config": - rule_value = {umo: rule_value} + override_paths = {} + remove_override_paths = [] + if rule_key == "session_service_config" and isinstance(rule_value, dict): + if "llm_enabled" in rule_value: + override_paths["provider_settings.enable"] = bool( + rule_value["llm_enabled"] + ) + if "tts_enabled" in rule_value: + override_paths["provider_tts_settings.enable"] = bool( + rule_value["tts_enabled"] + ) + if "persona_id" in rule_value: + override_paths["provider_settings.default_personality"] = ( + rule_value.get("persona_id") or "" + ) + if "session_enabled" in rule_value: + if rule_value["session_enabled"] is False: + override_paths["platform_settings.id_blacklist"] = [umo] + else: + remove_override_paths.append("platform_settings.id_blacklist") + if "custom_name" in rule_value: + custom_name = str(rule_value.get("custom_name") or "").strip() + alias = await self.db_helper.get_umo_alias(umo) + await self.db_helper.upsert_umo_alias( + umo, + alias.creator_sender_id if alias else "", + alias.auto_name if alias else None, + custom_name or None, + ) + elif rule_key == "session_plugin_config" and isinstance(rule_value, dict): + disabled_plugins = rule_value.get("disabled_plugins") + if isinstance(disabled_plugins, list): + override_paths["plugin_disabled_set"] = disabled_plugins + elif rule_key == "kb_config" and isinstance(rule_value, dict): + if "top_k" in rule_value: + override_paths["kb_final_top_k"] = rule_value["top_k"] + kb_ids = rule_value.get("kb_ids") + if isinstance(kb_ids, list): + override_paths["kb_names"] = [ + str(kb_id) for kb_id in kb_ids if str(kb_id).strip() + ] + else: + provider_path_map = { + f"provider_perf_{ProviderType.CHAT_COMPLETION.value}": ( + "provider_settings.default_provider_id" + ), + f"provider_perf_{ProviderType.SPEECH_TO_TEXT.value}": ( + "provider_stt_settings.provider_id" + ), + f"provider_perf_{ProviderType.TEXT_TO_SPEECH.value}": ( + "provider_tts_settings.provider_id" + ), + } + if rule_key in provider_path_map and rule_value: + override_paths[provider_path_map[rule_key]] = rule_value - await sp.session_put(umo, rule_key, rule_value) + if override_paths: + await self.core_lifecycle.astrbot_config_mgr.update_conf_overrides( + umo, + override_paths, + ) + if remove_override_paths: + await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides( + umo, + remove_override_paths, + ) return {"message": f"规则 {rule_key} 已更新", "umo": umo} async def delete_session_rule(self, data: object) -> dict: @@ -272,10 +616,62 @@ async def delete_session_rule(self, data: object) -> dict: if rule_key: if rule_key not in AVAILABLE_SESSION_RULE_KEYS: raise SessionManagementServiceError(f"不支持的规则键: {rule_key}") - await sp.session_remove(umo, rule_key) + if rule_key == "session_service_config": + await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides( + umo, + [ + "provider_settings.enable", + "provider_tts_settings.enable", + "provider_settings.default_personality", + "platform_settings.id_blacklist", + ], + ) + alias = await self.db_helper.get_umo_alias(umo) + if alias: + await self.db_helper.upsert_umo_alias( + umo, + alias.creator_sender_id, + alias.auto_name, + None, + ) + elif rule_key == "session_plugin_config": + await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides( + umo, + ["plugin_disabled_set"], + ) + elif rule_key == "kb_config": + await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides( + umo, + ["kb_names", "kb_final_top_k"], + ) + else: + provider_path_map = { + f"provider_perf_{ProviderType.CHAT_COMPLETION.value}": ( + "provider_settings.default_provider_id" + ), + f"provider_perf_{ProviderType.SPEECH_TO_TEXT.value}": ( + "provider_stt_settings.provider_id" + ), + f"provider_perf_{ProviderType.TEXT_TO_SPEECH.value}": ( + "provider_tts_settings.provider_id" + ), + } + if rule_key in provider_path_map: + await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides( + umo, + [provider_path_map[rule_key]], + ) return {"message": f"规则 {rule_key} 已删除", "umo": umo} await sp.clear_async("umo", umo) + alias = await self.db_helper.get_umo_alias(umo) + if alias: + await self.db_helper.upsert_umo_alias( + umo, + alias.creator_sender_id, + alias.auto_name, + None, + ) return {"message": "所有规则已删除", "umo": umo} async def delete_session_rules(self, data: object) -> dict: @@ -306,9 +702,9 @@ async def batch_delete_session_rule(self, data: object) -> dict: for umo in umos: try: if rule_key: - await sp.session_remove(umo, rule_key) + await self.delete_session_rule({"umo": umo, "rule_key": rule_key}) else: - await sp.clear_async("umo", umo) + await self.delete_session_rule({"umo": umo}) success_count += 1 except Exception as exc: logger.error(f"删除 umo {umo} 的规则失败: {exc!s}") @@ -458,24 +854,24 @@ async def batch_update_service(self, data: object) -> dict: for umo in umos: try: - session_config = ( - sp.get("session_service_config", {}, scope="umo", scope_id=umo) - or {} - ) - + override_paths = {} if llm_enabled is not None: - session_config["llm_enabled"] = llm_enabled + override_paths["provider_settings.enable"] = bool(llm_enabled) if tts_enabled is not None: - session_config["tts_enabled"] = tts_enabled + override_paths["provider_tts_settings.enable"] = bool(tts_enabled) if session_enabled is not None: - session_config["session_enabled"] = session_enabled - - sp.put( - "session_service_config", - session_config, - scope="umo", - scope_id=umo, - ) + if session_enabled is False: + override_paths["platform_settings.id_blacklist"] = [umo] + else: + await self.core_lifecycle.astrbot_config_mgr.remove_conf_overrides( + umo, + ["platform_settings.id_blacklist"], + ) + if override_paths: + await self.core_lifecycle.astrbot_config_mgr.update_conf_overrides( + umo, + override_paths, + ) success_count += 1 except Exception as exc: logger.error(f"更新 {umo} 服务状态失败: {exc!s}") diff --git a/dashboard/src/api/generated/openapi-v1/sdk.gen.ts b/dashboard/src/api/generated/openapi-v1/sdk.gen.ts index a75ccf577e..89639390fa 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, 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, 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, ListSessionConfigOverridesData, ListSessionConfigOverridesError, ListSessionConfigOverridesResponse, UpsertSessionConfigOverrideData, UpsertSessionConfigOverrideError, UpsertSessionConfigOverrideResponse, DeleteSessionConfigOverrideData, DeleteSessionConfigOverrideError, DeleteSessionConfigOverrideResponse, UpsertSessionAliasData, UpsertSessionAliasError, UpsertSessionAliasResponse, 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, ListUmoWorkspaceFilesData, ListUmoWorkspaceFilesError, ListUmoWorkspaceFilesResponse, 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()); @@ -2425,6 +2425,46 @@ export const deleteSessionRules = (options }); }; +/** + * List UMO config overrides + */ +export const listSessionConfigOverrides = (options?: OptionsLegacyParser) => { + return (options?.client ?? client).get({ + ...options, + url: '/api/v1/sessions/config-overrides' + }); +}; + +/** + * Update or create a UMO config override + */ +export const upsertSessionConfigOverride = (options: OptionsLegacyParser) => { + return (options?.client ?? client).post({ + ...options, + url: '/api/v1/sessions/config-overrides' + }); +}; + +/** + * Delete one or more UMO config overrides + */ +export const deleteSessionConfigOverride = (options: OptionsLegacyParser) => { + return (options?.client ?? client).post({ + ...options, + url: '/api/v1/sessions/config-overrides/delete' + }); +}; + +/** + * Update UMO aliases used as session remarks + */ +export const upsertSessionAlias = (options: OptionsLegacyParser) => { + return (options?.client ?? client).post({ + ...options, + url: '/api/v1/sessions/aliases' + }); +}; + /** * Batch update session provider selection */ @@ -2555,6 +2595,16 @@ export const exportConversations = (option }); }; +/** + * List workspace files for one UMO + */ +export const listUmoWorkspaceFiles = (options: OptionsLegacyParser) => { + return (options?.client ?? client).get({ + ...options, + url: '/api/v1/workspaces/by-umo' + }); +}; + /** * Get runtime statistics */ diff --git a/dashboard/src/api/generated/openapi-v1/types.gen.ts b/dashboard/src/api/generated/openapi-v1/types.gen.ts index 3b2314e4d1..06fb743770 100644 --- a/dashboard/src/api/generated/openapi-v1/types.gen.ts +++ b/dashboard/src/api/generated/openapi-v1/types.gen.ts @@ -550,6 +550,34 @@ export type ReorderRequest = { }>; }; +export type SessionAliasRequest = { + umo?: string; + umos?: Array<(string)>; + scope?: 'all' | 'group' | 'private' | 'custom_group'; + group_id?: string; + custom_name: string; + [key: string]: unknown | string; +}; + +export type scope = 'all' | 'group' | 'private' | 'custom_group'; + +export type SessionConfigOverrideDeleteRequest = { + umo?: string; + umos?: Array<(string)>; + scope?: 'all' | 'group' | 'private' | 'custom_group'; + group_id?: string; + path?: string; + paths?: Array<(string)>; + [key: string]: unknown | string; +}; + +export type SessionConfigOverrideRequest = { + umo: string; + path: string; + value: unknown; + [key: string]: unknown | string; +}; + export type SessionGroupRequest = { name?: string; umos?: Array<(string)>; @@ -623,8 +651,6 @@ export type UmoListRequest = { rule_key?: string; }; -export type scope = 'all' | 'group' | 'private' | 'custom_group'; - export type UpdateAccountRequest = { password: string; new_password?: string; @@ -2952,6 +2978,7 @@ export type ListSessionRulesData = { page?: number; page_size?: number; search?: string; + umo?: string; }; }; @@ -2975,6 +3002,43 @@ export type DeleteSessionRulesResponse = (SuccessEnvelope); export type DeleteSessionRulesError = unknown; +export type ListSessionConfigOverridesData = { + query?: { + page?: number; + page_size?: number; + search?: string; + umo?: string; + }; +}; + +export type ListSessionConfigOverridesResponse = (SuccessEnvelope); + +export type ListSessionConfigOverridesError = unknown; + +export type UpsertSessionConfigOverrideData = { + body: SessionConfigOverrideRequest; +}; + +export type UpsertSessionConfigOverrideResponse = (SuccessEnvelope); + +export type UpsertSessionConfigOverrideError = unknown; + +export type DeleteSessionConfigOverrideData = { + body: SessionConfigOverrideDeleteRequest; +}; + +export type DeleteSessionConfigOverrideResponse = (SuccessEnvelope); + +export type DeleteSessionConfigOverrideError = unknown; + +export type UpsertSessionAliasData = { + body: SessionAliasRequest; +}; + +export type UpsertSessionAliasResponse = (SuccessEnvelope); + +export type UpsertSessionAliasError = unknown; + export type BatchUpdateSessionProviderData = { body: BatchSessionProviderRequest; }; @@ -3124,6 +3188,17 @@ export type ExportConversationsResponse = (unknown); export type ExportConversationsError = unknown; +export type ListUmoWorkspaceFilesData = { + query: { + path?: string; + umo: string; + }; +}; + +export type ListUmoWorkspaceFilesResponse = (SuccessEnvelope); + +export type ListUmoWorkspaceFilesError = unknown; + export type GetStatsData = { query?: { offset_sec?: number; diff --git a/dashboard/src/api/v1.ts b/dashboard/src/api/v1.ts index d10f532c22..dc17fd1c16 100644 --- a/dashboard/src/api/v1.ts +++ b/dashboard/src/api/v1.ts @@ -34,6 +34,7 @@ import { type GhproxyTestRequest, type KnowledgeBaseCreateRequest, type KnowledgeBaseRequest, + type ListUmoWorkspaceFilesData, type LoginRequest, type ListConversationsData, type McpServerConfig, @@ -46,6 +47,9 @@ import { type BatchSessionProviderRequest, type BatchSessionServiceRequest, type SetupAuthRequest, + type SessionAliasRequest, + type SessionConfigOverrideDeleteRequest, + type SessionConfigOverrideRequest, type SessionGroupRequest, type SessionRuleRequest, type UmoListRequest, @@ -163,6 +167,7 @@ export interface SessionRuleListParams { page?: number; page_size?: number; search?: string; + umo?: string; } export interface ChatSessionListParams { @@ -942,12 +947,30 @@ export const sessionApi = { openApiV1.listSessionRules({ query: generatedQuery(params) }), ); }, + listConfigOverrides(params?: SessionRuleListParams) { + return typed( + openApiV1.listSessionConfigOverrides({ query: generatedQuery(params) }), + ); + }, upsertRule(payload: SessionRuleRequest) { return typed(openApiV1.upsertSessionRule({ body: payload })); }, deleteRules(payload: UmoListRequest) { return typed(openApiV1.deleteSessionRules({ body: payload })); }, + upsertConfigOverride(payload: SessionConfigOverrideRequest) { + return typed( + openApiV1.upsertSessionConfigOverride({ body: payload }), + ); + }, + deleteConfigOverride(payload: SessionConfigOverrideDeleteRequest) { + return typed( + openApiV1.deleteSessionConfigOverride({ body: payload }), + ); + }, + upsertAlias(payload: SessionAliasRequest) { + return typed(openApiV1.upsertSessionAlias({ body: payload })); + }, batchUpdateProvider(payload: BatchSessionProviderRequest) { return typed( openApiV1.batchUpdateSessionProvider({ body: payload }), @@ -1683,6 +1706,16 @@ export const conversationApi = { }, }; +export const workspaceApi = { + byUmo(params: ListUmoWorkspaceFilesData['query']) { + return typed( + openApiV1.listUmoWorkspaceFiles({ + query: params, + }), + ); + }, +}; + export const statsApi = { get(offsetSec?: number) { return typed( diff --git a/dashboard/src/components/shared/AstrBotConfigV4.vue b/dashboard/src/components/shared/AstrBotConfigV4.vue index 13b65e88f5..7fcbbb3559 100644 --- a/dashboard/src/components/shared/AstrBotConfigV4.vue +++ b/dashboard/src/components/shared/AstrBotConfigV4.vue @@ -323,7 +323,13 @@ function getSpecialSubtype(value) {
- {{ t('core.shared.pluginSetSelector.selectedPluginsLabel') }} + + {{ + itemKey.split('.').pop() === 'plugin_disabled_set' + ? t('core.shared.pluginSetSelector.disabledPluginsLabel') + : t('core.shared.pluginSetSelector.selectedPluginsLabel') + }} +
- {{ t('core.shared.pluginSetSelector.selectedPluginsLabel') }} + + {{ + itemKey.split('.').pop() === 'plugin_disabled_set' + ? t('core.shared.pluginSetSelector.disabledPluginsLabel') + : t('core.shared.pluginSetSelector.selectedPluginsLabel') + }} +