From 369eab18ab898f4339fe96d10061eecaaa5fc27d Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Wed, 13 Aug 2025 09:18:49 +0800 Subject: [PATCH] =?UTF-8?q?Refactor:=20=E9=87=8D=E6=9E=84=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6=E7=AE=A1=E7=90=86=EF=BC=8C=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=9B=B4=E7=81=B5=E6=B4=BB=E7=9A=84=E3=80=81?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E7=B2=92=E5=BA=A6=E7=9A=84=EF=BC=88=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=20umo=20part=EF=BC=89=E9=85=8D=E7=BD=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E9=9A=94=E7=A6=BB=20(#2328)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 重构配置文件管理,以支持更灵活的、基于 umo part 的配置文件隔离 * Refactor: 重构配置前端页面,新增数个配置项 (#2331) * refactor: 重构配置前端页面,新增数个配置项 * feat: 完善多配置文件结构 * perf: 系统配置入口 * fix: normal config item list not display * fix: 修复 axios 请求中的上下文引用问题 --- astrbot/core/astrbot_config_mgr.py | 250 +++++++ astrbot/core/config/default.py | 638 ++++++++++++---- astrbot/core/core_lifecycle.py | 52 +- astrbot/core/event_bus.py | 36 +- astrbot/core/persona_mgr.py | 9 +- astrbot/core/pipeline/context.py | 3 +- .../agent_runner/tool_loop_agent.py | 2 +- astrbot/core/platform/astr_message_event.py | 29 +- astrbot/core/platform/manager.py | 3 + astrbot/core/platform/message_session.py | 28 + astrbot/core/platform/platform.py | 2 +- astrbot/core/provider/func_tool_manager.py | 17 +- astrbot/core/provider/manager.py | 2 +- astrbot/core/star/context.py | 27 +- astrbot/dashboard/routes/config.py | 274 +++++-- astrbot/dashboard/routes/plugin.py | 16 - .../src/components/shared/AstrBotConfig.vue | 6 +- .../src/components/shared/AstrBotConfigV4.vue | 396 ++++++++++ .../src/components/shared/ListConfigItem.vue | 307 +++++--- .../src/components/shared/PersonaSelector.vue | 141 ++++ .../components/shared/ProviderSelector.vue | 150 ++++ .../i18n/locales/zh-CN/features/config.json | 6 +- .../full/vertical-sidebar/VerticalSidebar.vue | 3 +- dashboard/src/views/ConfigPage.vue | 691 ++++++++++++++---- dashboard/src/views/PlatformPage.vue | 5 - dashboard/src/views/ProviderPage.vue | 80 -- packages/astrbot/long_term_memory.py | 90 ++- packages/astrbot/main.py | 60 +- packages/session_controller/main.py | 15 +- packages/thinking_filter/main.py | 10 +- packages/web_searcher/main.py | 49 +- 31 files changed, 2611 insertions(+), 786 deletions(-) create mode 100644 astrbot/core/astrbot_config_mgr.py create mode 100644 astrbot/core/platform/message_session.py create mode 100644 dashboard/src/components/shared/AstrBotConfigV4.vue create mode 100644 dashboard/src/components/shared/PersonaSelector.vue create mode 100644 dashboard/src/components/shared/ProviderSelector.vue diff --git a/astrbot/core/astrbot_config_mgr.py b/astrbot/core/astrbot_config_mgr.py new file mode 100644 index 000000000..2e0c35855 --- /dev/null +++ b/astrbot/core/astrbot_config_mgr.py @@ -0,0 +1,250 @@ +import os +import uuid +from astrbot.core import AstrBotConfig, logger +from astrbot.core.utils.shared_preferences import SharedPreferences +from astrbot.core.config.astrbot_config import ASTRBOT_CONFIG_PATH +from astrbot.core.config.default import DEFAULT_CONFIG +from astrbot.core.platform.message_session import MessageSession +from astrbot.core.utils.astrbot_path import get_astrbot_config_path +from typing import TypeVar, TypedDict + +_VT = TypeVar("_VT") + + +class ConfInfo(TypedDict): + """Configuration information for a specific session or platform.""" + + id: str # UUID of the configuration or "default" + umop: list[str] # Unified Message Origin Pattern + name: str + path: str # File name to the configuration file + + +DEFAULT_CONFIG_CONF_INFO = ConfInfo( + id="default", + umop=["::"], + name="default", + path=ASTRBOT_CONFIG_PATH, +) + + +class AstrBotConfigManager: + """A class to manage the system configuration of AstrBot, aka ACM""" + + def __init__(self, default_config: AstrBotConfig, sp: SharedPreferences): + self.sp = sp + self.confs: dict[str, AstrBotConfig] = {} + """uuid / "default" -> AstrBotConfig""" + self.confs["default"] = default_config + self._load_all_configs() + + def _load_all_configs(self): + """Load all configurations from the shared preferences.""" + abconf_data = self.sp.get("abconf_mapping", {}) + for uuid_, meta in abconf_data.items(): + filename = meta["path"] + conf_path = os.path.join(get_astrbot_config_path(), filename) + if os.path.exists(conf_path): + conf = AstrBotConfig(config_path=conf_path) + self.confs[uuid_] = conf + else: + logger.warning( + f"Config file {conf_path} for UUID {uuid_} does not exist, skipping." + ) + continue + + def _is_umo_match(self, p1: str, p2: str) -> bool: + """判断 p2 umo 是否逻辑包含于 p1 umo""" + p1 = p1.split(":") + p2 = p2.split(":") + + if len(p1) != 3 or len(p2) != 3: + return False # 非法格式 + + return all(p == "" or p == t for p, t in zip(p1, p2)) + + def _load_conf_mapping(self, umo: str | MessageSession) -> ConfInfo: + """获取指定 umo 的配置文件 uuid, 如果不存在则返回默认配置(返回 "default") + + Returns: + ConfInfo: 包含配置文件的 uuid, 路径和名称等信息, 是一个 dict 类型 + """ + # uuid -> { "umop": list, "path": str, "name": str } + abconf_data = self.sp.get("abconf_mapping", {}) # default is not included here + if isinstance(umo, MessageSession): + umo = str(umo) + else: + umo = str(MessageSession.from_str(umo)) # validate + + for uuid_, meta in abconf_data.items(): + for pattern in meta["umop"]: + if self._is_umo_match(pattern, umo): + return ConfInfo(**meta, id=uuid_) + + return DEFAULT_CONFIG_CONF_INFO + + def _save_conf_mapping( + self, + abconf_path: str, + abconf_id: str, + umo_parts: list[str] | list[MessageSession], + abconf_name: str = None, + ) -> None: + """保存配置文件的映射关系""" + for part in umo_parts: + if isinstance(part, MessageSession): + part = str(part) + elif not isinstance(part, str): + raise ValueError( + "umo_parts must be a list of strings or MessageSession instances" + ) + abconf_data = self.sp.get("abconf_mapping", {}) + random_word = abconf_name or uuid.uuid4().hex[:8] + abconf_data[abconf_id] = { + "umop": umo_parts, + "path": abconf_path, + "name": random_word, + } + self.sp.put("abconf_mapping", abconf_data) + + def get_conf(self, umo: str | MessageSession) -> AstrBotConfig: + """获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。""" + if isinstance(umo, MessageSession): + umo = f"{umo.platform_id}:{umo.message_type}:{umo.session_id}" + + uuid_ = self._load_conf_mapping(umo)["id"] + + conf = self.confs.get(uuid_) + if not conf: + conf = self.confs["default"] # default MUST exists + + return conf + + @property + def default_conf(self) -> AstrBotConfig: + """获取默认配置文件""" + return self.confs["default"] + + def get_conf_info(self, umo: str | MessageSession) -> ConfInfo: + """获取指定 umo 的配置文件元数据""" + if isinstance(umo, MessageSession): + umo = f"{umo.platform_id}:{umo.message_type}:{umo.session_id}" + + return self._load_conf_mapping(umo) + + def get_conf_list(self) -> list[ConfInfo]: + """获取所有配置文件的元数据列表""" + conf_list = [] + conf_list.append(DEFAULT_CONFIG_CONF_INFO) + for uuid_, meta in self.sp.get("abconf_mapping", {}).items(): + conf_list.append(ConfInfo(**meta, id=uuid_)) + return conf_list + + def create_conf( + self, + umo_parts: list[str] | list[MessageSession], + config: dict = DEFAULT_CONFIG, + name: str = None, + ) -> str: + """ + umo 由三个部分组成 [platform_id]:[message_type]:[session_id]。 + + umo_parts 可以是 "::" (代表所有), 可以是 "[platform_id]::" (代表指定平台下的所有类型消息和会话)。 + """ + conf_uuid = str(uuid.uuid4()) + conf_file_name = f"abconf_{conf_uuid}.json" + conf_path = os.path.join(get_astrbot_config_path(), conf_file_name) + conf = AstrBotConfig(config_path=conf_path, default_config=config) + conf.save_config() + self._save_conf_mapping(conf_file_name, conf_uuid, umo_parts, abconf_name=name) + self.confs[conf_uuid] = conf + return conf_uuid + + def delete_conf(self, conf_id: str) -> bool: + """删除指定配置文件 + + Args: + conf_id: 配置文件的 UUID + + Returns: + bool: 删除是否成功 + + Raises: + ValueError: 如果试图删除默认配置文件 + """ + if conf_id == "default": + raise ValueError("不能删除默认配置文件") + + # 从映射中移除 + abconf_data = self.sp.get("abconf_mapping", {}) + if conf_id not in abconf_data: + logger.warning(f"配置文件 {conf_id} 不存在于映射中") + return False + + # 获取配置文件路径 + conf_path = os.path.join(get_astrbot_config_path(), abconf_data[conf_id]["path"]) + + # 删除配置文件 + try: + if os.path.exists(conf_path): + os.remove(conf_path) + logger.info(f"已删除配置文件: {conf_path}") + except Exception as e: + logger.error(f"删除配置文件 {conf_path} 失败: {e}") + return False + + # 从内存中移除 + if conf_id in self.confs: + del self.confs[conf_id] + + # 从映射中移除 + del abconf_data[conf_id] + self.sp.put("abconf_mapping", abconf_data) + + logger.info(f"成功删除配置文件 {conf_id}") + return True + + def update_conf_info(self, conf_id: str, name: str = None, umo_parts: list[str] = None) -> bool: + """更新配置文件信息 + + Args: + conf_id: 配置文件的 UUID + name: 新的配置文件名称 (可选) + umo_parts: 新的 UMO 部分列表 (可选) + + Returns: + bool: 更新是否成功 + """ + if conf_id == "default": + raise ValueError("不能更新默认配置文件的信息") + + abconf_data = self.sp.get("abconf_mapping", {}) + if conf_id not in abconf_data: + logger.warning(f"配置文件 {conf_id} 不存在于映射中") + return False + + # 更新名称 + if name is not None: + abconf_data[conf_id]["name"] = name + + # 更新 UMO 部分 + if umo_parts is not None: + # 验证 UMO 部分格式 + for part in umo_parts: + if isinstance(part, MessageSession): + part = str(part) + elif not isinstance(part, str): + raise ValueError("umo_parts must be a list of strings or MessageSession instances") + abconf_data[conf_id]["umop"] = umo_parts + + # 保存更新 + self.sp.put("abconf_mapping", abconf_data) + logger.info(f"成功更新配置文件 {conf_id} 的信息") + return True + + def g(self, umo: str = None, key: str = None, default: _VT = None) -> _VT: + """获取配置项。umo 为 None 时使用默认配置""" + if umo is None: + return self.confs["default"].get(key, default) + conf = self.get_conf(umo) + return conf.get(key, default) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 960ef107a..194264246 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -51,20 +51,25 @@ DEFAULT_CONFIG = { "provider_settings": { "enable": True, "default_provider_id": "", + "default_image_caption_provider_id": "", + "image_caption_prompt": "Please describe the image using Chinese.", + "provider_pool": ["all"], # "all" 表示使用所有可用的提供者 "wake_prefix": "", "web_search": False, + "websearch_provider": "default", + "websearch_tavily_key": "", "web_search_link": False, "display_reasoning_text": False, "identifier": False, "datetime_system_prompt": True, "default_personality": "default", + "persona_pool": ["all"], "prompt_prefix": "", "max_context_length": -1, "dequeue_context_length": 1, "streaming_response": False, "show_tool_use_status": False, "streaming_segmented": False, - "separate_provider": True, }, "provider_stt_settings": { "enable": False, @@ -80,13 +85,10 @@ DEFAULT_CONFIG = { "group_icl_enable": False, "group_message_max_cnt": 300, "image_caption": False, - "image_caption_provider_id": "", - "image_caption_prompt": "Please describe the image using Chinese.", "active_reply": { "enable": False, "method": "possibility_reply", "possibility_reply": 0.1, - "prompt": "", "whitelist": [], }, }, @@ -115,8 +117,8 @@ DEFAULT_CONFIG = { "log_level": "INFO", "pip_install_arg": "", "pypi_index_url": "https://mirrors.aliyun.com/pypi/simple/", - "persona": [], # deprecated - "timezone": "", + "persona": [], # deprecated + "timezone": "Asia/Shanghai", "callback_api_base": "", } @@ -124,7 +126,6 @@ DEFAULT_CONFIG = { # 配置项的中文描述、值类型 CONFIG_METADATA_2 = { "platform_group": { - "name": "消息平台", "metadata": { "platform": { "description": "消息平台适配器", @@ -382,152 +383,120 @@ CONFIG_METADATA_2 = { }, }, "platform_settings": { - "description": "平台设置", "type": "object", "items": { "plugin_enable": { "invisible": True, # 隐藏插件启用配置 }, "unique_session": { - "description": "会话隔离", "type": "bool", - "hint": "启用后,在群组或者频道中,每个人的消息上下文都是独立的。", }, "rate_limit": { - "description": "速率限制", - "hint": "每个会话在 `time` 秒内最多只能发送 `count` 条消息。", "type": "object", "items": { - "time": {"description": "消息速率限制时间", "type": "int"}, - "count": {"description": "消息速率限制计数", "type": "int"}, + "time": {"type": "int"}, + "count": {"type": "int"}, "strategy": { - "description": "速率限制策略", "type": "string", "options": ["stall", "discard"], - "hint": "当消息速率超过限制时的处理策略。stall 为等待,discard 为丢弃。", }, }, }, "no_permission_reply": { - "description": "无权限回复", "type": "bool", "hint": "启用后,当用户没有权限执行某个操作时,机器人会回复一条消息。", }, "empty_mention_waiting": { - "description": "只 @ 机器人是否触发等待", "type": "bool", "hint": "启用后,当消息内容只有 @ 机器人时,会触发等待,在 60 秒内的该用户的任意一条消息均会唤醒机器人。这在某些平台不支持 @ 和语音/图片等消息同时发送时特别有用。", }, "empty_mention_waiting_need_reply": { - "description": "只 @ 机器人触发等待时是否需要回复提醒", "type": "bool", "hint": "在上面一个配置项中,如果启用了触发等待,启用此项后,机器人会使用 LLM 生成一条回复。否则,将不回复而只是等待。", }, "friend_message_needs_wake_prefix": { - "description": "私聊消息是否需要唤醒前缀", "type": "bool", "hint": "启用后,私聊消息需要唤醒前缀才会被处理,同群聊一样。", }, "ignore_bot_self_message": { - "description": "是否忽略机器人自身的消息", "type": "bool", "hint": "某些平台会将自身账号在其他 APP 端发送的消息也当做消息事件下发导致给自己发消息时唤醒机器人", }, "ignore_at_all": { - "description": "是否忽略 @ 全体成员", "type": "bool", "hint": "启用后,机器人会忽略 @ 全体成员 的消息事件。", }, "segmented_reply": { - "description": "分段回复", "type": "object", "items": { "enable": { - "description": "启用分段回复", "type": "bool", }, "only_llm_result": { - "description": "仅对 LLM 结果分段", "type": "bool", }, "interval_method": { - "description": "间隔时间计算方法", "type": "string", "options": ["random", "log"], "hint": "分段回复的间隔时间计算方法。random 为随机时间,log 为根据消息长度计算,$y=log_(x)$,x为字数,y的单位为秒。", }, "interval": { - "description": "随机间隔时间(秒)", "type": "string", "hint": "`random` 方法用。每一段回复的间隔时间,格式为 `最小时间,最大时间`。如 `0.75,2.5`", }, "log_base": { - "description": "对数函数底数", "type": "float", "hint": "`log` 方法用。对数函数的底数。默认为 2.6", }, "words_count_threshold": { - "description": "字数阈值", "type": "int", "hint": "超过这个字数的消息不会被分段回复。默认为 150", }, "regex": { - "description": "正则表达式", "type": "string", "hint": "用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。re.findall(r'', text)", }, "content_cleanup_rule": { - "description": "过滤分段后的内容", "type": "string", "hint": "移除分段后的内容中的指定的内容。支持正则表达式。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。re.sub(r'', '', text)", }, }, }, "reply_prefix": { - "description": "回复前缀", "type": "string", "hint": "机器人回复消息时带有的前缀。", }, "forward_threshold": { - "description": "转发消息的字数阈值", "type": "int", "hint": "超过一定字数后,机器人会将消息折叠成 QQ 群聊的 “转发消息”,以防止刷屏。目前仅 QQ 平台适配器适用。", }, "enable_id_white_list": { - "description": "启用 ID 白名单", "type": "bool", }, "id_whitelist": { - "description": "ID 白名单", "type": "list", "items": {"type": "string"}, "hint": "只处理填写的 ID 发来的消息事件,为空时不启用。可使用 /sid 指令获取在平台上的会话 ID(类似 abc:GroupMessage:123)。管理员可使用 /wl 添加白名单", }, "id_whitelist_log": { - "description": "打印白名单日志", "type": "bool", "hint": "启用后,当一条消息没通过白名单时,会输出 INFO 级别的日志。", }, "wl_ignore_admin_on_group": { - "description": "管理员群组消息无视 ID 白名单", "type": "bool", }, "wl_ignore_admin_on_friend": { - "description": "管理员私聊消息无视 ID 白名单", "type": "bool", }, "reply_with_mention": { - "description": "回复时 @ 发送者", "type": "bool", "hint": "启用后,机器人回复消息时会 @ 发送者。实际效果以具体的平台适配器为准。", }, "reply_with_quote": { - "description": "回复时引用消息", "type": "bool", "hint": "启用后,机器人回复消息时会引用原消息。实际效果以具体的平台适配器为准。", }, "path_mapping": { - "description": "路径映射", "type": "list", "items": {"type": "string"}, "hint": "此功能解决由于文件系统不一致导致路径不存在的问题。格式为 <原路径>:<映射路径>。如 `/app/.config/QQ:/var/lib/docker/volumes/xxxx/_data`。这样,当消息平台下发的事件中图片和语音路径以 `/app/.config/QQ` 开头时,开头被替换为 `/var/lib/docker/volumes/xxxx/_data`。这在 AstrBot 或者平台协议端使用 Docker 部署时特别有用。", @@ -535,41 +504,33 @@ CONFIG_METADATA_2 = { }, }, "content_safety": { - "description": "内容安全", "type": "object", "items": { "also_use_in_response": { - "description": "对大模型响应安全审核", "type": "bool", "hint": "启用后,大模型的响应也会通过内容安全审核。", }, "baidu_aip": { - "description": "百度内容审核配置", "type": "object", "items": { "enable": { - "description": "启用百度内容审核", "type": "bool", "hint": "启用此功能前,您需要手动在设备中安装 baidu-aip 库。一般来说,安装指令如下: `pip3 install baidu-aip`", }, "app_id": {"description": "APP ID", "type": "string"}, "api_key": {"description": "API Key", "type": "string"}, "secret_key": { - "description": "Secret Key", "type": "string", }, }, }, "internal_keywords": { - "description": "内部关键词过滤", "type": "object", "items": { "enable": { - "description": "启用内部关键词过滤", "type": "bool", }, "extra_keywords": { - "description": "额外关键词", "type": "list", "items": {"type": "string"}, "hint": "额外的屏蔽关键词列表,支持正则表达式。", @@ -584,7 +545,6 @@ CONFIG_METADATA_2 = { "name": "服务提供商", "metadata": { "provider": { - "description": "服务提供商配置", "type": "list", "config_template": { "OpenAI": { @@ -1615,191 +1575,114 @@ CONFIG_METADATA_2 = { }, }, "provider_settings": { - "description": "大语言模型设置", "type": "object", "items": { "enable": { - "description": "启用大语言模型聊天", "type": "bool", - "hint": "如需切换大语言模型提供商,请使用 /provider 命令。", - }, - "separate_provider": { - "description": "提供商会话隔离", - "type": "bool", - "hint": "启用后,每个会话支持独立选择文本生成、STT、TTS 等提供商。如果会话在使用 /provider 指令时提示无权限,可以将会话加入管理员名单或者使用 /alter_cmd provider member 将指令设为非管理员指令。", }, "default_provider_id": { - "description": "默认模型提供商 ID", "type": "string", - "hint": "可选。每个聊天会话的默认提供商 ID。", }, "wake_prefix": { - "description": "LLM 聊天额外唤醒前缀", "type": "string", - "hint": "使用 LLM 聊天额外的触发条件。如填写 `chat`,则需要消息前缀加上 `/chat` 才能触发 LLM 聊天,是一个防止滥用的手段。", }, "web_search": { - "description": "启用网页搜索", "type": "bool", - "hint": "能访问 Google 时效果最佳(国内需要在 `其他配置` 开启 HTTP 代理)。如果 Google 访问失败,程序会依次访问 Bing, Sogo 搜索引擎。", }, "web_search_link": { - "description": "网页搜索引用链接", "type": "bool", - "hint": "开启后,将会传入网页搜索结果的链接给模型,并引导模型输出引用链接。", }, "display_reasoning_text": { - "description": "显示思考内容", "type": "bool", - "hint": "开启后,将在回复中显示模型的思考过程。", }, "identifier": { - "description": "启动识别群员", "type": "bool", - "hint": "在 Prompt 前加上群成员的名字以让模型更好地了解群聊状态。启用将略微增加 token 开销。", }, "datetime_system_prompt": { - "description": "启用日期时间系统提示", "type": "bool", - "hint": "启用后,会在系统提示词中加上当前机器的日期时间。", }, "default_personality": { - "description": "默认采用的人格情景的名称", "type": "string", - "hint": "", }, "prompt_prefix": { - "description": "Prompt 前缀文本", "type": "string", - "hint": "添加之后,会在每次对话的 Prompt 前加上此文本。", }, "max_context_length": { - "description": "最多携带对话数量(条)", "type": "int", - "hint": "超出这个数量时将丢弃最旧的部分,用户和AI的一轮聊天记为 1 条。-1 表示不限制,默认为不限制。", }, "dequeue_context_length": { - "description": "丢弃对话数量(条)", "type": "int", - "hint": "超出 最多携带对话数量(条) 时,丢弃多少条记录,用户和AI的一轮聊天记为 1 条。适宜的配置,可以提高超长上下文对话 deepseek 命中缓存效果,理想情况下计费将降低到1/3以下", }, "streaming_response": { - "description": "启用流式回复", "type": "bool", - "hint": "启用后,将会流式输出 LLM 的响应。目前仅支持 OpenAI API提供商 以及 Telegram、QQ Official 私聊 两个平台", }, "show_tool_use_status": { - "description": "函数调用状态输出", "type": "bool", - "hint": "在触发函数调用时输出其函数名和内容。", }, "streaming_segmented": { - "description": "不支持流式回复的平台分段输出", "type": "bool", - "hint": "启用后,若平台不支持流式回复,会分段输出。目前仅支持 aiocqhttp 两个平台,不支持或无需使用流式分段输出的平台会静默忽略此选项", }, }, }, "provider_stt_settings": { - "description": "语音转文本(STT)", "type": "object", "items": { "enable": { - "description": "启用语音转文本(STT)", "type": "bool", - "hint": "启用前请在 服务提供商配置 处创建支持 语音转文本任务 的提供商。如 whisper。", }, "provider_id": { - "description": "提供商 ID", "type": "string", - "hint": "语音转文本提供商 ID。如果不填写将使用载入的第一个提供商。", }, }, }, "provider_tts_settings": { - "description": "文本转语音(TTS)", "type": "object", "items": { "enable": { - "description": "启用文本转语音(TTS)", "type": "bool", - "hint": "启用前请在 服务提供商配置 处创建支持 语音转文本任务 的提供商。如 openai_tts。", }, "provider_id": { - "description": "提供商 ID", "type": "string", - "hint": "文本转语音提供商 ID。如果不填写将使用载入的第一个提供商。", }, "dual_output": { - "description": "启用语音和文字双输出", "type": "bool", - "hint": "启用后,Bot 将同时输出语音和文字消息。", }, "use_file_service": { - "description": "使用文件服务提供 TTS 语音文件", "type": "bool", - "hint": "启用后,如已配置 callback_api_base ,将会使用文件服务提供TTS语音文件", }, }, }, "provider_ltm_settings": { - "description": "聊天记忆增强(Beta)", "type": "object", "items": { "group_icl_enable": { - "description": "群聊内记录各群员对话", "type": "bool", - "hint": "启用后,会记录群聊内各群员的对话。使用 /reset 命令清除记录。推荐使用 gpt-4o-mini 模型。", }, "group_message_max_cnt": { - "description": "群聊消息最大数量", "type": "int", - "hint": "群聊消息最大数量。超过此数量后,会自动清除旧消息。", }, "image_caption": { - "description": "群聊图像转述(需模型支持)", "type": "bool", - "hint": "用模型将群聊中的图片消息转述为文字,推荐 gpt-4o-mini 模型。和机器人的唤醒聊天中的图片消息仍然会直接作为上下文输入。", - }, - "image_caption_provider_id": { - "description": "图像转述提供商 ID", - "type": "string", - "hint": "可选。图像转述提供商 ID。如为空将选择聊天使用的提供商。", }, "image_caption_prompt": { - "description": "图像转述提示词", "type": "string", }, "active_reply": { - "description": "主动回复", "type": "object", "items": { "enable": { - "description": "启用主动回复", "type": "bool", - "hint": "启用后,会根据触发概率主动回复群聊内的对话。QQ官方API(qq_official)不可用", }, "whitelist": { - "description": "主动回复白名单", "type": "list", "items": {"type": "string"}, - "hint": "启用后,只有在白名单内的群聊会被主动回复。为空时不启用白名单过滤。需要通过 /sid 获取 SID 添加到这里。", }, "method": { - "description": "回复方法", "type": "string", "options": ["possibility_reply"], - "hint": "回复方法。possibility_reply 为根据概率回复", }, "possibility_reply": { - "description": "回复概率", "type": "float", - "hint": "回复概率。当回复方法为 possibility_reply 时有效。当概率 >= 1 时,每条消息都会回复。", - }, - "prompt": { - "description": "提示词", - "type": "string", - "hint": "提示词。当提示词为空时,如果触发回复,则向 LLM 请求的是触发的消息的内容;否则是提示词。此项可以和定时回复(暂未实现)配合使用。", }, }, }, @@ -1808,81 +1691,528 @@ CONFIG_METADATA_2 = { }, }, "misc_config_group": { - "name": "其他配置", "metadata": { "wake_prefix": { - "description": "机器人唤醒前缀", "type": "list", "items": {"type": "string"}, - "hint": "在不 @ 机器人的情况下,可以通过外加消息前缀来唤醒机器人。更改此配置将影响整个 Bot 的功能唤醒,包括所有指令。如果您不保留 `/`,则内置指令(help等)将需要通过您的唤醒前缀来触发。", }, "t2i": { - "description": "文本转图像", "type": "bool", - "hint": "启用后,超出一定长度的文本将会通过 AstrBot API 渲染成 Markdown 图片发送。可以缓解审核和消息过长刷屏的问题,并提高 Markdown 文本的可读性。", }, "t2i_word_threshold": { - "description": "文本转图像字数阈值", "type": "int", - "hint": "超出此字符长度的文本将会被转换成图片。字数不能低于 50。", }, "admins_id": { - "description": "管理员 ID", "type": "list", "items": {"type": "string"}, - "hint": "管理员 ID 列表,管理员可以使用一些特权命令,如 `update`, `plugin` 等。ID 可以通过 `/sid` 指令获得。回车添加,可添加多个。", }, "http_proxy": { - "description": "HTTP 代理", "type": "string", - "hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`", }, "timezone": { - "description": "时区", "type": "string", - "hint": "时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab", }, "callback_api_base": { - "description": "对外可达的回调接口地址", "type": "string", - "hint": "外部服务可能会通过 AstrBot 生成的回调链接(如文件下载链接)访问 AstrBot 后端。由于 AstrBot 无法自动判断部署环境中对外可达的主机地址(host),因此需要通过此配置项显式指定 “外部服务如何访问 AstrBot” 的地址。如 http://localhost:6185,https://example.com 等。", }, "log_level": { - "description": "控制台日志级别", "type": "string", - "hint": "控制台输出日志的级别。", "options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], }, "t2i_strategy": { - "description": "文本转图像渲染源", "type": "string", - "hint": "文本转图像策略。`remote` 为使用远程基于 HTML 的渲染服务,`local` 为使用 PIL 本地渲染。当使用 local 时,将 ttf 字体命名为 'font.ttf' 放在 data/ 目录下可自定义字体。", "options": ["remote", "local"], }, "t2i_endpoint": { - "description": "文本转图像服务接口", "type": "string", - "hint": "当 t2i_strategy 为 remote 时生效。为空时使用 AstrBot API 服务", }, "t2i_use_file_service": { - "description": "本地文本转图像使用文件服务提供文件", "type": "bool", - "hint": "当 t2i_strategy 为 local 并且配置 callback_api_base 时生效。是否使用文件服务提供文件。", }, "pip_install_arg": { - "description": "pip 安装参数", "type": "string", - "hint": "安装插件依赖时,会使用 Python 的 pip 工具。这里可以填写额外的参数,如 `--break-system-package` 等。", }, "pypi_index_url": { - "description": "PyPI 软件仓库地址", "type": "string", - "hint": "安装 Python 依赖时请求的 PyPI 软件仓库地址。默认为 https://mirrors.aliyun.com/pypi/simple/", }, }, }, } + +CONFIG_METADATA_3 = { + "ai_group": { + "name": "AI 配置", + "metadata": { + "ai": { + "description": "模型", + "type": "object", + "items": { + "provider_settings.enable": { + "description": "启用大语言模型聊天", + "type": "bool", + }, + "provider_settings.default_provider_id": { + "description": "默认聊天模型", + "type": "string", + "_special": "select_provider", + "hint": "留空时使用第一个模型。", + }, + "provider_settings.default_image_caption_provider_id": { + "description": "默认图片转述模型", + "type": "string", + "_special": "select_provider", + "hint": "留空代表不使用。可用于不支持视觉模态的聊天模型。", + }, + "provider_stt_settings.provider_id": { + "description": "语音转文本模型", + "type": "string", + "hint": "留空代表不使用。", + "_special": "select_provider_stt", + }, + "provider_tts_settings.provider_id": { + "description": "文本转语音模型", + "type": "string", + "hint": "留空代表不使用。", + "_special": "select_provider_tts", + }, + "provider_settings.image_caption_prompt": { + "description": "图片转述提示词", + "type": "text", + }, + }, + }, + "persona": { + "description": "人格", + "type": "object", + "items": { + "provider_settings.default_personality": { + "description": "默认采用的人格", + "type": "string", + "_special": "select_persona", + }, + }, + }, + "websearch": { + "description": "网页搜索", + "type": "object", + "items": { + "provider_settings.web_search": { + "description": "启用网页搜索", + "type": "bool", + }, + "provider_settings.websearch_provider": { + "description": "网页搜索提供商", + "type": "string", + "options": ["default", "tavily"], + }, + "provider_settings.websearch_tavily_key": { + "description": "Tavily API Key", + "type": "string", + "condition": { + "provider_settings.websearch_provider": "tavily", + }, + }, + "provider_settings.web_search_link": { + "description": "显示来源引用", + "type": "bool", + }, + }, + }, + "others": { + "description": "其他配置", + "type": "object", + "items": { + "provider_settings.display_reasoning_text": { + "description": "显示思考内容", + "type": "bool", + }, + "provider_settings.identifier": { + "description": "用户感知", + "type": "bool", + }, + "provider_settings.datetime_system_prompt": { + "description": "现实世界时间感知", + "type": "bool", + }, + "provider_settings.show_tool_use_status": { + "description": "输出函数调用状态", + "type": "bool", + }, + "provider_settings.streaming_response": { + "description": "流式回复", + "type": "bool", + }, + "provider_settings.streaming_segmented": { + "description": "不支持流式回复的平台采取分段输出", + "type": "bool", + }, + "provider_settings.max_context_length": { + "description": "最多携带对话轮数", + "type": "int", + "hint": "超出这个数量时丢弃最旧的部分,一轮聊天记为 1 条。-1 为不限制。", + }, + "provider_settings.dequeue_context_length": { + "description": "丢弃对话轮数", + "type": "int", + "hint": "超出最多携带对话轮数时, 一次丢弃的聊天轮数。", + }, + "provider_settings.wake_prefix": { + "description": "LLM 聊天额外唤醒前缀 ", + "type": "string", + }, + "provider_settings.prompt_prefix": { + "description": "额外前缀提示词", + "type": "string", + }, + "provider_settings.dual_output": { + "description": "开启 TTS 时同时输出语音和文字内容", + "type": "bool", + }, + }, + }, + }, + }, + "platform_group": { + "name": "平台配置", + "metadata": { + "general": { + "description": "基本", + "type": "object", + "items": { + "admins_id": { + "description": "管理员 ID", + "type": "list", + "items": {"type": "string"}, + }, + "platform_settings.unique_session": { + "description": "隔离会话", + "type": "bool", + "hint": "启用后,群成员的上下文独立。", + }, + "wake_prefix": { + "description": "唤醒词", + "type": "list", + "items": {"type": "string"}, + }, + "platform_settings.friend_message_needs_wake_prefix": { + "description": "私聊消息需要唤醒词", + "type": "bool", + }, + "platform_settings.reply_prefix": { + "description": "回复时的文本前缀", + "type": "string", + }, + "platform_settings.reply_with_mention": { + "description": "回复时 @ 发送人", + "type": "bool", + }, + "platform_settings.reply_with_quote": { + "description": "回复时引用发送人消息", + "type": "bool", + }, + "platform_settings.forward_threshold": { + "description": "转发消息的字数阈值", + "type": "int", + }, + "platform_settings.empty_mention_waiting": { + "description": "只 @ 机器人是否触发等待", + "type": "bool", + }, + }, + }, + "whitelist": { + "description": "白名单", + "type": "object", + "items": { + "platform_settings.enable_id_white_list": { + "description": "启用白名单", + "type": "bool", + "hint": "启用后,只有在白名单内的会话会被响应。", + }, + "platform_settings.id_whitelist": { + "description": "白名单 ID 列表", + "type": "list", + "items": {"type": "string"}, + "hint": "使用 /sid 获取 ID。", + }, + "platform_settings.id_whitelist_log": { + "description": "输出日志", + "type": "bool", + "hint": "启用后,当一条消息没通过白名单时,会输出 INFO 级别的日志。", + }, + "platform_settings.wl_ignore_admin_on_group": { + "description": "管理员群组消息无视 ID 白名单", + "type": "bool", + }, + "platform_settings.wl_ignore_admin_on_friend": { + "description": "管理员私聊消息无视 ID 白名单", + "type": "bool", + }, + }, + }, + "rate_limit": { + "description": "速率限制", + "type": "object", + "items": { + "platform_settings.rate_limit.time": { + "description": "消息速率限制时间(秒)", + "type": "int", + }, + "platform_settings.rate_limit.count": { + "description": "消息速率限制计数", + "type": "int", + }, + "platform_settings.rate_limit.strategy": { + "description": "速率限制策略", + "type": "string", + "options": ["stall", "discard"], + }, + }, + }, + "content_safety": { + "description": "内容安全", + "type": "object", + "items": { + "platform_settings.content_safety.also_use_in_response": { + "description": "同时检查模型的响应内容", + "type": "bool", + }, + "platform_settings.content_safety.baidu_aip.enable": { + "description": "使用百度内容安全审核", + "type": "bool", + "hint": "您需要手动安装 baidu-aip 库。", + }, + "platform_settings.content_safety.baidu_aip.app_id": { + "description": "App ID", + "type": "string", + "condition": { + "platform_settings.content_safety.baidu_aip.enable": True, + }, + }, + "platform_settings.content_safety.baidu_aip.api_key": { + "description": "API Key", + "type": "string", + "condition": { + "platform_settings.content_safety.baidu_aip.enable": True, + }, + }, + "platform_settings.content_safety.baidu_aip.secret_key": { + "description": "Secret Key", + "type": "string", + "condition": { + "platform_settings.content_safety.baidu_aip.enable": True, + }, + }, + "platform_settings.content_safety.internal_keywords.enable": { + "description": "关键词检查", + "type": "bool", + }, + "platform_settings.content_safety.internal_keywords.extra_keywords": { + "description": "额外关键词", + "type": "list", + "items": {"type": "string"}, + "hint": "额外的屏蔽关键词列表,支持正则表达式。", + }, + }, + }, + "t2i": { + "description": "文本转图像", + "type": "object", + "items": { + "t2i": { + "description": "文本转图像输出", + "type": "bool", + }, + "t2i_word_threshold": { + "description": "文本转图像字数阈值", + "type": "int", + }, + }, + }, + "others": { + "description": "其他配置", + "type": "object", + "items": { + "platform_settings.ignore_bot_self_message": { + "description": "是否忽略机器人自身的消息", + "type": "bool", + }, + "platform_settings.ignore_at_all": { + "description": "是否忽略 @ 全体成员事件", + "type": "bool", + }, + "platform_settings.no_permission_reply": { + "description": "用户权限不足时是否回复", + "type": "bool", + }, + }, + }, + }, + }, + "ext_group": { + "name": "扩展配置", + "metadata": { + "segmented_reply": { + "description": "分段回复", + "type": "object", + "items": { + "platform_settings.segmented_reply.enable": { + "description": "启用分段回复", + "type": "bool", + }, + "platform_settings.segmented_reply.only_llm_result": { + "description": "仅对 LLM 结果分段", + "type": "bool", + }, + "platform_settings.segmented_reply.interval_method": { + "description": "间隔方法", + "type": "string", + "options": ["random", "log"], + }, + "platform_settings.segmented_reply.interval": { + "description": "随机间隔时间", + "type": "string", + "hint": "格式:最小值,最大值(如:1.5,3.5)", + "condition": { + "platform_settings.segmented_reply.interval_method": "random", + }, + }, + "platform_settings.segmented_reply.log_base": { + "description": "对数底数", + "type": "float", + "hint": "对数间隔的底数,默认为 2.0。取值范围为 1.0-10.0。", + "condition": { + "platform_settings.segmented_reply.interval_method": "log", + }, + }, + "platform_settings.segmented_reply.words_count_threshold": { + "description": "分段回复字数阈值", + "type": "int", + }, + "platform_settings.segmented_reply.regex": { + "description": "分段正则表达式", + "type": "string", + }, + "platform_settings.segmented_reply.content_cleanup_rule": { + "description": "内容过滤正则表达式", + "type": "string", + "hint": "移除分段后内容中的指定内容。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。", + }, + }, + }, + "ltm": { + "description": "群聊上下文感知(原聊天记忆增强)", + "type": "object", + "items": { + "provider_ltm_settings.group_icl_enable": { + "description": "启用群聊上下文感知", + "type": "bool", + }, + "provider_ltm_settings.group_message_max_cnt": { + "description": "最大消息数量", + "type": "int", + }, + "provider_ltm_settings.image_caption": { + "description": "自动理解图片", + "type": "bool", + "hint": "需要设置默认图片转述模型。", + }, + "provider_ltm_settings.active_reply.enable": { + "description": "主动回复", + "type": "bool", + }, + "provider_ltm_settings.active_reply.method": { + "description": "主动回复方法", + "type": "string", + "options": ["possibility_reply"], + "condition": { + "provider_ltm_settings.active_reply.enable": True, + }, + }, + "provider_ltm_settings.active_reply.possibility_reply": { + "description": "回复概率", + "type": "float", + "hint": "0.0-1.0 之间的数值", + "condition": { + "provider_ltm_settings.active_reply.enable": True, + }, + }, + "provider_ltm_settings.active_reply.whitelist": { + "description": "主动回复白名单", + "type": "list", + "items": {"type": "string"}, + "hint": "为空时不启用白名单过滤。使用 /sid 获取 ID。", + "condition": { + "provider_ltm_settings.active_reply.enable": True, + }, + }, + }, + }, + }, + }, +} + +CONFIG_METADATA_3_SYSTEM = { + "system_group": { + "name": "系统配置", + "metadata": { + "system": { + "description": "系统配置", + "type": "object", + "items": { + "t2i_strategy": { + "description": "文本转图像策略", + "type": "string", + "hint": "文本转图像策略。`remote` 为使用远程基于 HTML 的渲染服务,`local` 为使用 PIL 本地渲染。当使用 local 时,将 ttf 字体命名为 'font.ttf' 放在 data/ 目录下可自定义字体。", + "options": ["remote", "local"], + }, + "t2i_endpoint": { + "description": "文本转图像服务接口", + "type": "string", + "hint": "为空时使用 AstrBot API 服务", + "condition": { + "t2i_strategy": "remote", + }, + }, + "log_level": { + "description": "控制台日志级别", + "type": "string", + "hint": "控制台输出日志的级别。", + "options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + }, + "pip_install_arg": { + "description": "pip 安装额外参数", + "type": "string", + "hint": "安装插件依赖时,会使用 Python 的 pip 工具。这里可以填写额外的参数,如 `--break-system-package` 等。", + }, + "pypi_index_url": { + "description": "PyPI 软件仓库地址", + "type": "string", + "hint": "安装 Python 依赖时请求的 PyPI 软件仓库地址。默认为 https://mirrors.aliyun.com/pypi/simple/", + }, + "callback_api_base": { + "description": "对外可达的回调接口地址", + "type": "string", + "hint": "外部服务可能会通过 AstrBot 生成的回调链接(如文件下载链接)访问 AstrBot 后端。由于 AstrBot 无法自动判断部署环境中对外可达的主机地址(host),因此需要通过此配置项显式指定 “外部服务如何访问 AstrBot” 的地址。如 http://localhost:6185,https://example.com 等。", + }, + "timezone": { + "description": "时区", + "type": "string", + "hint": "时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab", + }, + "http_proxy": { + "description": "HTTP 代理", + "type": "string", + "hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`", + }, + }, + } + }, + } +} + + DEFAULT_VALUE_MAP = { "int": 0, "float": 0.0, diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index 28545a238..8cfb8a7ad 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -27,10 +27,11 @@ from astrbot.core.provider.manager import ProviderManager from astrbot.core import LogBroker from astrbot.core.db import BaseDatabase from astrbot.core.updator import AstrBotUpdator -from astrbot.core import logger +from astrbot.core import logger, sp from astrbot.core.config.default import VERSION from astrbot.core.conversation_mgr import ConversationManager from astrbot.core.platform_message_history_mgr import PlatformMessageHistoryManager +from astrbot.core.astrbot_config_mgr import AstrBotConfigManager from astrbot.core.star.star_handler import star_handlers_registry, EventType from astrbot.core.star.star_handler import star_map from astrbot.core.db.migration.helper import do_migration_v4 @@ -76,11 +77,16 @@ class AstrBotCoreLifecycle: except Exception as e: logger.error(f"迁移到 v4.0.0 新版本数据格式失败: {e}") + # 初始化 AstrBot 配置管理器 + self.astrbot_config_mgr = AstrBotConfigManager( + default_config=self.astrbot_config, sp=sp + ) + # 初始化事件队列 self.event_queue = Queue() # 初始化人格管理器 - self.persona_mgr = PersonaManager(self.db, self.astrbot_config) + self.persona_mgr = PersonaManager(self.db, self.astrbot_config_mgr) await self.persona_mgr.initialize() # 初始化供应商管理器 @@ -107,6 +113,7 @@ class AstrBotCoreLifecycle: self.conversation_manager, self.platform_message_history_manager, self.persona_mgr, + self.astrbot_config_mgr, ) # 初始化插件管理器 @@ -119,17 +126,16 @@ class AstrBotCoreLifecycle: await self.provider_manager.initialize() # 初始化消息事件流水线调度器 - self.pipeline_scheduler = PipelineScheduler( - PipelineContext(self.astrbot_config, self.plugin_manager) - ) - await self.pipeline_scheduler.initialize() - self.star_context.pipeline_ctx = self.pipeline_scheduler.ctx + + self.pipeline_scheduler_mapping = await self.load_pipeline_scheduler() # 初始化更新器 self.astrbot_updator = AstrBotUpdator() # 初始化事件总线 - self.event_bus = EventBus(self.event_queue, self.pipeline_scheduler) + self.event_bus = EventBus( + self.event_queue, self.pipeline_scheduler_mapping, self.astrbot_config_mgr + ) # 记录启动时间 self.start_time = int(time.time()) @@ -252,3 +258,33 @@ class AstrBotCoreLifecycle: ) ) return tasks + + async def load_pipeline_scheduler(self) -> dict[str, PipelineScheduler]: + """加载消息事件流水线调度器 + + Returns: + dict[str, PipelineScheduler]: 平台 ID 到流水线调度器的映射 + """ + mapping = {} + for conf_id, ab_config in self.astrbot_config_mgr.confs.items(): + scheduler = PipelineScheduler( + PipelineContext(ab_config, self.plugin_manager, conf_id) + ) + await scheduler.initialize() + mapping[conf_id] = scheduler + return mapping + + async def reload_pipeline_scheduler(self, conf_id: str): + """重新加载消息事件流水线调度器 + + Returns: + dict[str, PipelineScheduler]: 平台 ID 到流水线调度器的映射 + """ + ab_config = self.astrbot_config_mgr.confs.get(conf_id) + if not ab_config: + raise ValueError(f"配置文件 {conf_id} 不存在") + scheduler = PipelineScheduler( + PipelineContext(ab_config, self.plugin_manager, conf_id) + ) + await scheduler.initialize() + self.pipeline_scheduler_mapping[conf_id] = scheduler diff --git a/astrbot/core/event_bus.py b/astrbot/core/event_bus.py index 5010a0645..2ae709396 100644 --- a/astrbot/core/event_bus.py +++ b/astrbot/core/event_bus.py @@ -16,30 +16,32 @@ from asyncio import Queue from astrbot.core.pipeline.scheduler import PipelineScheduler from astrbot.core import logger from .platform import AstrMessageEvent +from astrbot.core.astrbot_config_mgr import AstrBotConfigManager class EventBus: - """事件总线: 用于处理事件的分发和处理 + """用于处理事件的分发和处理""" - 维护一个异步队列, 来接受各种消息事件 - """ - - def __init__(self, event_queue: Queue, pipeline_scheduler: PipelineScheduler): + def __init__( + self, + event_queue: Queue, + pipeline_scheduler_mapping: dict[str, PipelineScheduler], + astrbot_config_mgr: AstrBotConfigManager = None, + ): self.event_queue = event_queue # 事件队列 - self.pipeline_scheduler = pipeline_scheduler # 管道调度器 + # abconf uuid -> scheduler + self.pipeline_scheduler_mapping = pipeline_scheduler_mapping + self.astrbot_config_mgr = astrbot_config_mgr async def dispatch(self): - """无限循环的调度函数, 从事件队列中获取新的事件, 打印日志并创建一个新的异步任务来执行管道调度器的处理逻辑""" while True: - event: AstrMessageEvent = ( - await self.event_queue.get() - ) # 从事件队列中获取新的事件 - self._print_event(event) # 打印日志 - asyncio.create_task( - self.pipeline_scheduler.execute(event) - ) # 创建新的异步任务来执行管道调度器的处理逻辑 + event: AstrMessageEvent = await self.event_queue.get() + conf_info = self.astrbot_config_mgr.get_conf_info(event.unified_msg_origin) + self._print_event(event, conf_info["name"]) + scheduler = self.pipeline_scheduler_mapping.get(conf_info["id"]) + asyncio.create_task(scheduler.execute(event)) - def _print_event(self, event: AstrMessageEvent): + def _print_event(self, event: AstrMessageEvent, conf_name: str): """用于记录事件信息 Args: @@ -48,10 +50,10 @@ class EventBus: # 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要 if event.get_sender_name(): logger.info( - f"[{event.get_platform_id()}({event.get_platform_name()})] {event.get_sender_name()}/{event.get_sender_id()}: {event.get_message_outline()}" + f"[{conf_name}] [{event.get_platform_id()}({event.get_platform_name()})] {event.get_sender_name()}/{event.get_sender_id()}: {event.get_message_outline()}" ) # 没有发送者名称: [平台名] 发送者ID: 消息概要 else: logger.info( - f"[{event.get_platform_id()}({event.get_platform_name()})] {event.get_sender_id()}: {event.get_message_outline()}" + f"[{conf_name}] [{event.get_platform_id()}({event.get_platform_name()})] {event.get_sender_id()}: {event.get_message_outline()}" ) diff --git a/astrbot/core/persona_mgr.py b/astrbot/core/persona_mgr.py index dc322c789..17f428f8d 100644 --- a/astrbot/core/persona_mgr.py +++ b/astrbot/core/persona_mgr.py @@ -1,15 +1,14 @@ from astrbot.core.db import BaseDatabase from astrbot.core.db.po import Persona, Personality -from astrbot.core.config import AstrBotConfig +from astrbot.core.astrbot_config_mgr import AstrBotConfigManager from astrbot import logger class PersonaManager: - def __init__(self, db_helper: BaseDatabase, astrbot_config: AstrBotConfig): + def __init__(self, db_helper: BaseDatabase, acm: AstrBotConfigManager): self.db = db_helper - self.config = astrbot_config - _ps: dict = astrbot_config["provider_settings"] - self.default_persona: str = _ps.get("default_personality", "default") + default_ps = acm.default_conf.get("provider_settings", {}) + self.default_persona: str = default_ps.get("default_personality", "default") self.personas: list[Persona] = [] self.selected_default_persona: Persona | None = None diff --git a/astrbot/core/pipeline/context.py b/astrbot/core/pipeline/context.py index 932c5d5c2..d1a1fc397 100644 --- a/astrbot/core/pipeline/context.py +++ b/astrbot/core/pipeline/context.py @@ -2,7 +2,7 @@ import inspect import traceback import typing as T from dataclasses import dataclass -from astrbot.core.config.astrbot_config import AstrBotConfig +from astrbot.core.config import AstrBotConfig from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.star import PluginManager from astrbot.api import logger @@ -17,6 +17,7 @@ class PipelineContext: astrbot_config: AstrBotConfig # AstrBot 配置对象 plugin_manager: PluginManager # 插件管理器对象 + astrbot_config_id: str async def call_event_hook( self, diff --git a/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py b/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py index cd705275e..a4223eb8a 100644 --- a/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py +++ b/astrbot/core/pipeline/process_stage/agent_runner/tool_loop_agent.py @@ -197,7 +197,7 @@ class ToolLoopAgent(BaseAgentRunner): logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}") executor = func_tool.execute( event=self.event, - pipeline_context=self.pipeline_ctx, + call_handler_func=self.pipeline_ctx.call_handler, **func_tool_args, ) async for resp in executor: diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py index 5efbfb2f6..73ec81420 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -3,7 +3,7 @@ import asyncio import re import hashlib import uuid -from dataclasses import dataclass + from typing import List, Union, Optional, AsyncGenerator from astrbot.core.db.po import Conversation @@ -23,32 +23,7 @@ from astrbot.core.provider.entities import ProviderRequest from astrbot.core.utils.metrics import Metric from .astrbot_message import AstrBotMessage, Group from .platform_metadata import PlatformMetadata - - -@dataclass -class MessageSession: - """描述一条消息在 AstrBot 中对应的会话的唯一标识。 - 如果您需要实例化 MessageSession,请不要给 platform_id 赋值(或者同时给 platform_name 和 platform_id 赋值相同值)。它会在 __post_init__ 中自动设置为 platform_name 的值。""" - - platform_name: str - """平台适配器实例的唯一标识符。自 AstrBot v4.0.0 起,该字段实际为 platform_id。""" - message_type: MessageType - session_id: str - platform_id: str = None - - def __str__(self): - return f"{self.platform_id}:{self.message_type.value}:{self.session_id}" - - def __post_init__(self): - self.platform_id = self.platform_name - - @staticmethod - def from_str(session_str: str): - platform_id, message_type, session_id = session_str.split(":") - return MessageSession(platform_id, MessageType(message_type), session_id) - - -MessageSesion = MessageSession # back compatibility +from .message_session import MessageSession, MessageSesion # noqa class AstrMessageEvent(abc.ABC): diff --git a/astrbot/core/platform/manager.py b/astrbot/core/platform/manager.py index 23109ca53..62328e881 100644 --- a/astrbot/core/platform/manager.py +++ b/astrbot/core/platform/manager.py @@ -18,6 +18,9 @@ class PlatformManager: self.platforms_config = config["platform"] self.settings = config["platform_settings"] + """NOTE: 这里是 default 的配置文件,以保证最大的兼容性; + 这个配置中的 unique_session 需要特殊处理, + 约定整个项目中对 unique_session 的引用都从 default 的配置中获取""" self.event_queue = event_queue async def initialize(self): diff --git a/astrbot/core/platform/message_session.py b/astrbot/core/platform/message_session.py new file mode 100644 index 000000000..bf5a72a9a --- /dev/null +++ b/astrbot/core/platform/message_session.py @@ -0,0 +1,28 @@ +from astrbot.core.platform.message_type import MessageType +from dataclasses import dataclass + + +@dataclass +class MessageSession: + """描述一条消息在 AstrBot 中对应的会话的唯一标识。 + 如果您需要实例化 MessageSession,请不要给 platform_id 赋值(或者同时给 platform_name 和 platform_id 赋值相同值)。它会在 __post_init__ 中自动设置为 platform_name 的值。""" + + platform_name: str + """平台适配器实例的唯一标识符。自 AstrBot v4.0.0 起,该字段实际为 platform_id。""" + message_type: MessageType + session_id: str + platform_id: str = None + + def __str__(self): + return f"{self.platform_id}:{self.message_type.value}:{self.session_id}" + + def __post_init__(self): + self.platform_id = self.platform_name + + @staticmethod + def from_str(session_str: str): + platform_id, message_type, session_id = session_str.split(":") + return MessageSession(platform_id, MessageType(message_type), session_id) + + +MessageSesion = MessageSession # back compatibility diff --git a/astrbot/core/platform/platform.py b/astrbot/core/platform/platform.py index 6ed53fe0e..c109f29b4 100644 --- a/astrbot/core/platform/platform.py +++ b/astrbot/core/platform/platform.py @@ -5,7 +5,7 @@ from asyncio import Queue from .platform_metadata import PlatformMetadata from .astr_message_event import AstrMessageEvent from astrbot.core.message.message_event_result import MessageChain -from .astr_message_event import MessageSesion +from .message_session import MessageSesion from astrbot.core.utils.metrics import Metric diff --git a/astrbot/core/provider/func_tool_manager.py b/astrbot/core/provider/func_tool_manager.py index c6e78e190..296e9d227 100644 --- a/astrbot/core/provider/func_tool_manager.py +++ b/astrbot/core/provider/func_tool_manager.py @@ -34,7 +34,6 @@ from typing_extensions import TYPE_CHECKING if TYPE_CHECKING: from astrbot.core.platform.astr_message_event import AstrMessageEvent - from astrbot.core.pipeline.context import PipelineContext DEFAULT_MCP_CONFIG = {"mcpServers": {}} @@ -80,14 +79,14 @@ class FunctionTool: async def execute( self, event: AstrMessageEvent = None, - pipeline_context: "PipelineContext" = None, + call_handler_func: Awaitable = None, **tool_args, ) -> AsyncGenerator[Any | mcp.types.CallToolResult, None]: """执行函数调用。 Args: event (AstrMessageEvent): 事件对象, 当 origin 为 local 时必须提供。 - pipeline_context (PipelineContext): 流水线调度器上下文, 当 origin 为 local 时必须提供。 + call_handler_func (AsyncGenerator): 用于调用处理函数的异步生成器, 当 origin 为 mcp 时必须提供。 **kwargs: 函数调用的参数。 Returns: @@ -96,7 +95,7 @@ class FunctionTool: if self.origin == "local": if not event: raise ValueError("Event must be provided for local function tools.") - wrapper = pipeline_context.call_handler( + wrapper = call_handler_func( event=event, handler=self.handler, **tool_args, @@ -207,7 +206,7 @@ class ToolSet: """Get all function tools.""" return self.get_tool(name) - def openai_schema(self, omit_empty_parameters: bool = False) -> List[Dict]: + def openai_schema(self, omit_empty_parameter_field: bool = False) -> List[Dict]: """Convert tools to OpenAI API function calling schema format.""" result = [] for tool in self.tools: @@ -219,7 +218,7 @@ class ToolSet: }, } - if tool.parameters.get("properties") or not omit_empty_parameters: + if tool.parameters.get("properties") or not omit_empty_parameter_field: func_def["function"]["parameters"] = tool.parameters result.append(func_def) @@ -319,8 +318,8 @@ class ToolSet: return declarations @deprecated(reason="Use openai_schema() instead", version="4.0.0") - def get_func_desc_openai_style(self, omit_empty_parameters: bool = False): - return self.openai_schema(omit_empty_parameters) + def get_func_desc_openai_style(self, omit_empty_parameter_field: bool = False): + return self.openai_schema(omit_empty_parameter_field) @deprecated(reason="Use anthropic_schema() instead", version="4.0.0") def get_func_desc_anthropic_style(self): @@ -817,7 +816,7 @@ class FunctionToolManager: """ tools = [f for f in self.func_list if f.active] toolset = ToolSet(tools) - return toolset.openai_schema(omit_empty_parameters=omit_empty_parameter_field) + return toolset.openai_schema(omit_empty_parameter_field=omit_empty_parameter_field) def get_func_desc_anthropic_style(self) -> list: """ diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 7d7779e81..0f2d9fb86 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -82,7 +82,7 @@ class ProviderManager: """ if provider_id not in self.inst_map: raise ValueError(f"提供商 {provider_id} 不存在,无法设置。") - if umo and self.provider_settings["separate_provider"]: + if umo: perf = sp.get("session_provider_perf", {}) session_perf = perf.get(umo, {}) session_perf[provider_type.value] = provider_id diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index 76cdea062..f9ed440cb 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -18,6 +18,7 @@ from astrbot.core.provider.manager import ProviderManager from astrbot.core.platform import Platform from astrbot.core.platform.manager import PlatformManager from astrbot.core.platform_message_history_mgr import PlatformMessageHistoryManager +from astrbot.core.astrbot_config_mgr import AstrBotConfigManager from astrbot.core.persona_mgr import PersonaManager from .star import star_registry, StarMetadata, star_map from .star_handler import star_handlers_registry, StarHandlerMetadata, EventType @@ -31,11 +32,6 @@ from astrbot.core.star.filter.platform_adapter_type import ( ) from deprecated import deprecated -from typing_extensions import TYPE_CHECKING - -if TYPE_CHECKING: - from astrbot.core.pipeline.context import PipelineContext - class Context: """ @@ -57,8 +53,6 @@ class Context: registered_web_apis: list = [] - pipeline_ctx: "PipelineContext" = None - # back compatibility _register_tasks: List[Awaitable] = [] _star_manager = None @@ -73,6 +67,7 @@ class Context: conversation_manager: ConversationManager = None, message_history_manager: PlatformMessageHistoryManager = None, persona_manager: PersonaManager = None, + astrbot_config_mgr: AstrBotConfigManager = None, ): self._event_queue = event_queue self._config = config @@ -82,6 +77,7 @@ class Context: self.conversation_manager = conversation_manager self.message_history_manager = message_history_manager self.persona_manager = persona_manager + self.astrbot_config_mgr = astrbot_config_mgr def get_registered_star(self, star_name: str) -> StarMetadata: """根据插件名获取插件的 Metadata""" @@ -145,7 +141,7 @@ class Context: Args: umo(str): unified_message_origin 值,如果传入并且用户启用了提供商会话隔离,则使用该会话偏好的提供商。 """ - if umo and self._config["provider_settings"]["separate_provider"]: + if umo: perf = sp.get("session_provider_perf", {}) prov_id = perf.get(umo, {}).get(ProviderType.CHAT_COMPLETION.value, None) if inst := self.provider_manager.inst_map.get(prov_id, None): @@ -159,7 +155,7 @@ class Context: Args: umo(str): unified_message_origin 值,如果传入,则使用该会话偏好的提供商。 """ - if umo and self._config["provider_settings"]["separate_provider"]: + if umo: perf = sp.get("session_provider_perf", {}) prov_id = perf.get(umo, {}).get(ProviderType.TEXT_TO_SPEECH.value, None) if inst := self.provider_manager.inst_map.get(prov_id, None): @@ -173,16 +169,20 @@ class Context: Args: umo(str): unified_message_origin 值,如果传入,则使用该会话偏好的提供商。 """ - if umo and self._config["provider_settings"]["separate_provider"]: + if umo: perf = sp.get("session_provider_perf", {}) prov_id = perf.get(umo, {}).get(ProviderType.SPEECH_TO_TEXT.value, None) if inst := self.provider_manager.inst_map.get(prov_id, None): return inst return self.provider_manager.curr_stt_provider_inst - def get_config(self) -> AstrBotConfig: + def get_config(self, umo: str = None) -> AstrBotConfig: """获取 AstrBot 的配置。""" - return self._config + if not umo: + # using default config + return self._config + else: + return self.astrbot_config_mgr.get_conf(umo) def get_db(self) -> BaseDatabase: """获取 AstrBot 数据库。""" @@ -257,9 +257,6 @@ class Context: return True return False - def get_pipeline_context(self) -> "PipelineContext": - return self.pipeline_ctx - """ 以下的方法已经不推荐使用。请从 AstrBot 文档查看更好的注册方式。 """ diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index 11b474860..a897154f6 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -4,7 +4,12 @@ import os from .route import Route, Response, RouteContext from astrbot.core.provider.entities import ProviderType from quart import request -from astrbot.core.config.default import CONFIG_METADATA_2, DEFAULT_VALUE_MAP +from astrbot.core.config.default import ( + CONFIG_METADATA_2, + DEFAULT_VALUE_MAP, + CONFIG_METADATA_3, + CONFIG_METADATA_3_SYSTEM, +) from astrbot.core.utils.astrbot_path import get_astrbot_path from astrbot.core.config.astrbot_config import AstrBotConfig from astrbot.core.core_lifecycle import AstrBotCoreLifecycle @@ -159,32 +164,119 @@ class ConfigRoute(Route): super().__init__(context) self.core_lifecycle = core_lifecycle self.config: AstrBotConfig = core_lifecycle.astrbot_config + self.acm = core_lifecycle.astrbot_config_mgr self.routes = { + "/config/abconf/new": ("POST", self.create_abconf), + "/config/abconf": ("GET", self.get_abconf), + "/config/abconfs": ("GET", self.get_abconf_list), + "/config/abconf/delete": ("POST", self.delete_abconf), + "/config/abconf/update": ("POST", self.update_abconf), "/config/get": ("GET", self.get_configs), "/config/astrbot/update": ("POST", self.post_astrbot_configs), "/config/plugin/update": ("POST", self.post_plugin_configs), "/config/platform/new": ("POST", self.post_new_platform), "/config/platform/update": ("POST", self.post_update_platform), "/config/platform/delete": ("POST", self.post_delete_platform), + "/config/platform/list": ("GET", self.get_platform_list), "/config/provider/new": ("POST", self.post_new_provider), "/config/provider/update": ("POST", self.post_update_provider), "/config/provider/delete": ("POST", self.post_delete_provider), "/config/provider/check_one": ("GET", self.check_one_provider_status), "/config/provider/list": ("GET", self.get_provider_config_list), "/config/provider/model_list": ("GET", self.get_provider_model_list), - "/config/provider/get_session_seperate": ( - "GET", - lambda: Response() - .ok({"enable": self.config["provider_settings"]["separate_provider"]}) - .__dict__, - ), - "/config/provider/set_session_seperate": ( - "POST", - self.post_session_seperate, - ), } self.register_routes() + async def get_abconf_list(self): + """获取所有 AstrBot 配置文件的列表""" + abconf_list = self.acm.get_conf_list() + return Response().ok({"info_list": abconf_list}).__dict__ + + async def create_abconf(self): + """创建新的 AstrBot 配置文件""" + post_data = await request.json + if not post_data: + return Response().error("缺少配置数据").__dict__ + umo_parts = post_data["umo_parts"] + name = post_data.get("name", None) + + try: + conf_id = self.acm.create_conf(umo_parts=umo_parts, name=name) + return Response().ok(message="创建成功", data={"conf_id": conf_id}).__dict__ + except ValueError as e: + return Response().error(str(e)).__dict__ + + async def get_abconf(self): + """获取指定 AstrBot 配置文件""" + abconf_id = request.args.get("id") + system_config = request.args.get("system_config", "0").lower() == "1" + if not abconf_id and not system_config: + return Response().error("缺少配置文件 ID").__dict__ + + try: + if system_config: + abconf = self.acm.confs["default"] + return ( + Response() + .ok({"config": abconf, "metadata": CONFIG_METADATA_3_SYSTEM}) + .__dict__ + ) + abconf = self.acm.confs[abconf_id] + return ( + Response() + .ok({"config": abconf, "metadata": CONFIG_METADATA_3}) + .__dict__ + ) + except ValueError as e: + return Response().error(str(e)).__dict__ + + async def delete_abconf(self): + """删除指定 AstrBot 配置文件""" + post_data = await request.json + if not post_data: + return Response().error("缺少配置数据").__dict__ + + conf_id = post_data.get("id") + if not conf_id: + return Response().error("缺少配置文件 ID").__dict__ + + try: + success = self.acm.delete_conf(conf_id) + if success: + return Response().ok(message="删除成功").__dict__ + else: + return Response().error("删除失败").__dict__ + except ValueError as e: + return Response().error(str(e)).__dict__ + except Exception as e: + logger.error(traceback.format_exc()) + return Response().error(f"删除配置文件失败: {str(e)}").__dict__ + + async def update_abconf(self): + """更新指定 AstrBot 配置文件信息""" + post_data = await request.json + if not post_data: + return Response().error("缺少配置数据").__dict__ + + conf_id = post_data.get("id") + if not conf_id: + return Response().error("缺少配置文件 ID").__dict__ + + name = post_data.get("name") + umo_parts = post_data.get("umo_parts") + + try: + success = self.acm.update_conf_info(conf_id, name=name, umo_parts=umo_parts) + if success: + return Response().ok(message="更新成功").__dict__ + else: + return Response().error("更新失败").__dict__ + except ValueError as e: + return Response().error(str(e)).__dict__ + except Exception as e: + logger.error(traceback.format_exc()) + return Response().error(f"更新配置文件失败: {str(e)}").__dict__ + async def _test_single_provider(self, provider): """辅助函数:测试单个 provider 的可用性""" meta = provider.meta() @@ -209,11 +301,16 @@ class ConfigRoute(Route): response = await asyncio.wait_for( provider.text_chat(prompt="REPLY `PONG` ONLY"), timeout=45.0 ) - logger.debug(f"Received response from {status_info['name']}: {response}") + logger.debug( + f"Received response from {status_info['name']}: {response}" + ) if response is not None: status_info["status"] = "available" response_text_snippet = "" - if hasattr(response, "completion_text") and response.completion_text: + if ( + hasattr(response, "completion_text") + and response.completion_text + ): response_text_snippet = ( response.completion_text[:70] + "..." if len(response.completion_text) > 70 @@ -232,29 +329,48 @@ class ConfigRoute(Route): f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'" ) else: - status_info["error"] = "Test call returned None, but expected an LLMResponse object." - logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None.") + status_info["error"] = ( + "Test call returned None, but expected an LLMResponse object." + ) + logger.warning( + f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None." + ) except asyncio.TimeoutError: - status_info["error"] = "Connection timed out after 45 seconds during test call." - logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) timed out.") + status_info["error"] = ( + "Connection timed out after 45 seconds during test call." + ) + logger.warning( + f"Provider {status_info['name']} (ID: {status_info['id']}) timed out." + ) except Exception as e: error_message = str(e) status_info["error"] = error_message - logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}") - logger.debug(f"Traceback for {status_info['name']}:\n{traceback.format_exc()}") + logger.warning( + f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}" + ) + logger.debug( + f"Traceback for {status_info['name']}:\n{traceback.format_exc()}" + ) elif provider_capability_type == ProviderType.EMBEDDING: try: # For embedding, we can call the get_embedding method with a short prompt. embedding_result = await provider.get_embedding("health_check") - if isinstance(embedding_result, list) and (not embedding_result or isinstance(embedding_result[0], float)): + if isinstance(embedding_result, list) and ( + not embedding_result or isinstance(embedding_result[0], float) + ): status_info["status"] = "available" else: status_info["status"] = "unavailable" - status_info["error"] = f"Embedding test failed: unexpected result type {type(embedding_result)}" + status_info["error"] = ( + f"Embedding test failed: unexpected result type {type(embedding_result)}" + ) except Exception as e: - logger.error(f"Error testing embedding provider {provider_name}: {e}", exc_info=True) + logger.error( + f"Error testing embedding provider {provider_name}: {e}", + exc_info=True, + ) status_info["status"] = "unavailable" status_info["error"] = f"Embedding test failed: {str(e)}" @@ -266,41 +382,71 @@ class ConfigRoute(Route): status_info["status"] = "available" else: status_info["status"] = "unavailable" - status_info["error"] = f"TTS test failed: unexpected result type {type(audio_result)}" + status_info["error"] = ( + f"TTS test failed: unexpected result type {type(audio_result)}" + ) except Exception as e: - logger.error(f"Error testing TTS provider {provider_name}: {e}", exc_info=True) + logger.error( + f"Error testing TTS provider {provider_name}: {e}", exc_info=True + ) status_info["status"] = "unavailable" status_info["error"] = f"TTS test failed: {str(e)}" elif provider_capability_type == ProviderType.SPEECH_TO_TEXT: try: - logger.debug(f"Sending health check audio to provider: {status_info['name']}") - sample_audio_path = os.path.join(get_astrbot_path(), "samples", "stt_health_check.wav") + logger.debug( + f"Sending health check audio to provider: {status_info['name']}" + ) + sample_audio_path = os.path.join( + get_astrbot_path(), "samples", "stt_health_check.wav" + ) if not os.path.exists(sample_audio_path): status_info["status"] = "unavailable" - status_info["error"] = "STT test failed: sample audio file not found." - logger.warning(f"STT test for {status_info['name']} failed: sample audio file not found at {sample_audio_path}") + status_info["error"] = ( + "STT test failed: sample audio file not found." + ) + logger.warning( + f"STT test for {status_info['name']} failed: sample audio file not found at {sample_audio_path}" + ) else: text_result = await provider.get_text(sample_audio_path) if isinstance(text_result, str) and text_result: status_info["status"] = "available" - snippet = text_result[:70] + "..." if len(text_result) > 70 else text_result - logger.info(f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{snippet}'") + snippet = ( + text_result[:70] + "..." + if len(text_result) > 70 + else text_result + ) + logger.info( + f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{snippet}'" + ) else: status_info["status"] = "unavailable" - status_info["error"] = f"STT test failed: unexpected result type {type(text_result)}" - logger.warning(f"STT test for {status_info['name']} failed: unexpected result type {type(text_result)}") + status_info["error"] = ( + f"STT test failed: unexpected result type {type(text_result)}" + ) + logger.warning( + f"STT test for {status_info['name']} failed: unexpected result type {type(text_result)}" + ) except Exception as e: - logger.error(f"Error testing STT provider {provider_name}: {e}", exc_info=True) + logger.error( + f"Error testing STT provider {provider_name}: {e}", exc_info=True + ) status_info["status"] = "unavailable" status_info["error"] = f"STT test failed: {str(e)}" else: - logger.debug(f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}") + logger.debug( + f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}" + ) status_info["status"] = "available" - status_info["error"] = "This provider type is not tested and is assumed to be available." + status_info["error"] = ( + "This provider type is not tested and is assumed to be available." + ) return status_info - def _error_response(self, message: str, status_code: int = 500, log_fn=logger.error): + def _error_response( + self, message: str, status_code: int = 500, log_fn=logger.error + ): log_fn(message) # 记录更详细的traceback信息,但只在是严重错误时 if status_code == 500: @@ -311,7 +457,9 @@ class ConfigRoute(Route): """API: check a single LLM Provider's status by id""" provider_id = request.args.get("id") if not provider_id: - return self._error_response("Missing provider_id parameter", 400, logger.warning) + return self._error_response( + "Missing provider_id parameter", 400, logger.warning + ) logger.info(f"API call: /config/provider/check_one id={provider_id}") try: @@ -319,16 +467,21 @@ class ConfigRoute(Route): target = prov_mgr.inst_map.get(provider_id) if not target: - logger.warning(f"Provider with id '{provider_id}' not found in provider_manager.") - return Response().error(f"Provider with id '{provider_id}' not found").__dict__ + logger.warning( + f"Provider with id '{provider_id}' not found in provider_manager." + ) + return ( + Response() + .error(f"Provider with id '{provider_id}' not found") + .__dict__ + ) result = await self._test_single_provider(target) return Response().ok(result).__dict__ except Exception as e: return self._error_response( - f"Critical error checking provider {provider_id}: {e}", - 500 + f"Critical error checking provider {provider_id}: {e}", 500 ) async def get_configs(self): @@ -339,21 +492,6 @@ class ConfigRoute(Route): return Response().ok(await self._get_astrbot_config()).__dict__ return Response().ok(await self._get_plugin_config(plugin_name)).__dict__ - async def post_session_seperate(self): - """设置提供商会话隔离""" - post_config = await request.json - enable = post_config.get("enable", None) - if enable is None: - return Response().error("缺少参数 enable").__dict__ - - astrbot_config = self.core_lifecycle.astrbot_config - astrbot_config["provider_settings"]["separate_provider"] = enable - try: - astrbot_config.save_config() - except Exception as e: - return Response().error(str(e)).__dict__ - return Response().ok(None, "设置成功~").__dict__ - async def get_provider_config_list(self): provider_type = request.args.get("provider_type", None) if not provider_type: @@ -387,11 +525,21 @@ class ConfigRoute(Route): logger.error(traceback.format_exc()) return Response().error(str(e)).__dict__ + async def get_platform_list(self): + """获取所有平台的列表""" + platform_list = [] + for platform in self.config["platform"]: + platform_list.append(platform) + return Response().ok({"platforms": platform_list}).__dict__ + async def post_astrbot_configs(self): - post_configs = await request.json + data = await request.json + config = data.get("config", None) + conf_id = data.get("conf_id", None) try: - await self._save_astrbot_configs(post_configs) - return Response().ok(None, "保存成功~ 机器人正在重载配置。").__dict__ + await self._save_astrbot_configs(config, conf_id) + await self.core_lifecycle.reload_pipeline_scheduler(conf_id) + return Response().ok(None, "保存成功~").__dict__ except Exception as e: logger.error(traceback.format_exc()) return Response().error(str(e)).__dict__ @@ -550,10 +698,12 @@ class ConfigRoute(Route): return ret - async def _save_astrbot_configs(self, post_configs: dict): + async def _save_astrbot_configs(self, post_configs: dict, conf_id: str = None): try: - save_config(post_configs, self.config, is_core=True) - await self.core_lifecycle.restart() + if conf_id not in self.acm.confs: + raise ValueError(f"配置文件 {conf_id} 不存在") + astrbot_config = self.acm.confs[conf_id] + save_config(post_configs, astrbot_config, is_core=True) except Exception as e: raise e diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index 179b45428..c02767672 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -286,14 +286,6 @@ class PluginRoute(Route): f"{filter.parent_command_names[0]} {filter.command_name}" ) info["cmd"] = info["cmd"].strip() - if ( - self.core_lifecycle.astrbot_config["wake_prefix"] - and len(self.core_lifecycle.astrbot_config["wake_prefix"]) - > 0 - ): - info["cmd"] = ( - f"{self.core_lifecycle.astrbot_config['wake_prefix'][0]}{info['cmd']}" - ) elif isinstance(filter, CommandGroupFilter): info["type"] = "指令组" info["cmd"] = filter.get_complete_command_names()[0] @@ -301,14 +293,6 @@ class PluginRoute(Route): info["sub_command"] = filter.print_cmd_tree( filter.sub_command_filters ) - if ( - self.core_lifecycle.astrbot_config["wake_prefix"] - and len(self.core_lifecycle.astrbot_config["wake_prefix"]) - > 0 - ): - info["cmd"] = ( - f"{self.core_lifecycle.astrbot_config['wake_prefix'][0]}{info['cmd']}" - ) elif isinstance(filter, RegexFilter): info["type"] = "正则匹配" info["cmd"] = filter.regex_str diff --git a/dashboard/src/components/shared/AstrBotConfig.vue b/dashboard/src/components/shared/AstrBotConfig.vue index 5610e7bc4..8ca49397c 100644 --- a/dashboard/src/components/shared/AstrBotConfig.vue +++ b/dashboard/src/components/shared/AstrBotConfig.vue @@ -176,7 +176,7 @@ function saveEditedContent() { @@ -287,9 +287,9 @@ function saveEditedContent() { > - diff --git a/dashboard/src/components/shared/AstrBotConfigV4.vue b/dashboard/src/components/shared/AstrBotConfigV4.vue new file mode 100644 index 000000000..e4cafc591 --- /dev/null +++ b/dashboard/src/components/shared/AstrBotConfigV4.vue @@ -0,0 +1,396 @@ + + + + + + + diff --git a/dashboard/src/components/shared/ListConfigItem.vue b/dashboard/src/components/shared/ListConfigItem.vue index 76d446ed4..96c0ba372 100644 --- a/dashboard/src/components/shared/ListConfigItem.vue +++ b/dashboard/src/components/shared/ListConfigItem.vue @@ -1,135 +1,216 @@ - \ No newline at end of file diff --git a/dashboard/src/components/shared/PersonaSelector.vue b/dashboard/src/components/shared/PersonaSelector.vue new file mode 100644 index 000000000..a87dabc5e --- /dev/null +++ b/dashboard/src/components/shared/PersonaSelector.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/dashboard/src/components/shared/ProviderSelector.vue b/dashboard/src/components/shared/ProviderSelector.vue new file mode 100644 index 000000000..5cec8c20e --- /dev/null +++ b/dashboard/src/components/shared/ProviderSelector.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/dashboard/src/i18n/locales/zh-CN/features/config.json b/dashboard/src/i18n/locales/zh-CN/features/config.json index 8d059c90f..dfec81c9d 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config.json @@ -30,7 +30,11 @@ "configApplyError": "配置未应用,Json 格式错误。", "saveSuccess": "配置保存成功", "saveError": "配置保存失败", - "loadError": "配置加载失败" + "loadError": "配置加载失败", + "deleteSuccess": "删除成功", + "deleteError": "删除失败", + "updateSuccess": "更新成功", + "updateError": "更新失败" }, "sections": { "general": "常规设置", diff --git a/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue b/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue index 07d2ea563..e1c9f62d8 100644 --- a/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue +++ b/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue @@ -1,6 +1,5 @@