From 72f917d611315e9d41ab5f3a1ef35a15130ba283 Mon Sep 17 00:00:00 2001 From: anka <1350989414@qq.com> Date: Mon, 7 Apr 2025 17:31:57 +0800 Subject: [PATCH 01/31] =?UTF-8?q?fix:=20gemini=E5=8F=AA=E5=9C=A8content?= =?UTF-8?q?=E4=B8=8D=E4=B8=BA=E7=A9=BA=E7=9A=84=E6=97=B6=E5=80=99=E5=8A=A0?= =?UTF-8?q?=E5=85=A5=E4=B8=8A=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/provider/sources/gemini_source.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index 9f5f7c3c1..7ae418938 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -146,12 +146,10 @@ class ProviderGoogleGenAI(Provider): for message in payloads["messages"]: if message["role"] == "user": if isinstance(message["content"], str): - if not message["content"]: - message["content"] = "" - - google_genai_conversation.append( - {"role": "user", "parts": [{"text": message["content"]}]} - ) + if message["content"]: + google_genai_conversation.append( + {"role": "user", "parts": [{"text": message["content"]}]} + ) elif isinstance(message["content"], list): # images parts = [] @@ -175,11 +173,10 @@ class ProviderGoogleGenAI(Provider): elif message["role"] == "assistant": if "content" in message: - if not message["content"]: - message["content"] = "" - google_genai_conversation.append( - {"role": "model", "parts": [{"text": message["content"]}]} - ) + if message["content"]: + google_genai_conversation.append( + {"role": "model", "parts": [{"text": message["content"]}]} + ) elif "tool_calls" in message: # tool calls in the last turn parts = [] From b9a983f8e0555641f94b6b4aef4ac85f6c45ee46 Mon Sep 17 00:00:00 2001 From: anka <1350989414@qq.com> Date: Mon, 7 Apr 2025 17:45:35 +0800 Subject: [PATCH 02/31] =?UTF-8?q?fix:=20=E4=B8=BA=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=A0=87=E8=AE=B0,=20=E4=B8=8D=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=E5=85=A5=E4=B8=8A=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../process_stage/method/llm_request.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 674a7fd79..987e82570 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -58,12 +58,16 @@ class LLMRequestSubStage(Stage): if event.get_extra("provider_request"): req = event.get_extra("provider_request") - assert isinstance(req, ProviderRequest), ( - "provider_request 必须是 ProviderRequest 类型。" - ) + assert isinstance( + req, ProviderRequest + ), "provider_request 必须是 ProviderRequest 类型。" if req.conversation: - req.contexts = json.loads(req.conversation.history) + all_contexts = json.loads(req.conversation.history) + # 对函数工具调用做过滤 + req.contexts = [ + msg for msg in all_contexts if "_tool_call_history" not in msg + ] else: req = ProviderRequest(prompt="", image_urls=[]) if self.provider_wake_prefix: @@ -312,9 +316,12 @@ class LLMRequestSubStage(Stage): contexts = req.contexts contexts.append(await req.assemble_context()) - # tool calls result + # 记录并标记函数调用结果 if req.tool_calls_result: - contexts.extend(req.tool_calls_result.to_openai_messages()) + tool_calls_messages = req.tool_calls_result.to_openai_messages() + for message in tool_calls_messages: + message["_tool_call_history"] = True + contexts.extend(tool_calls_messages) contexts.append( {"role": "assistant", "content": llm_response.completion_text} From d88420dd03da6c04cca56e15f21fc57660de1f0e Mon Sep 17 00:00:00 2001 From: anka <1350989414@qq.com> Date: Mon, 7 Apr 2025 17:55:12 +0800 Subject: [PATCH 03/31] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E4=BA=BA=E7=B1=BB=E5=8F=AF=E8=AF=BB=E7=9A=84=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E7=9A=84=E9=80=BB=E8=BE=91,=20=E5=8C=BA?= =?UTF-8?q?=E5=88=86=E5=87=BD=E6=95=B0=E8=B0=83=E7=94=A8(=E6=97=A0contents?= =?UTF-8?q?)=E5=92=8C=E4=B8=80=E8=88=AC=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/conversation_mgr.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py index c506fa8f1..b0f5c136d 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -175,7 +175,15 @@ class ConversationManager: if record["role"] == "user": temp_contexts.append(f"User: {record['content']}") elif record["role"] == "assistant": - temp_contexts.append(f"Assistant: {record['content']}") + if "content" in record and record["content"]: + temp_contexts.append(f"Assistant: {record['content']}") + elif "tool_calls" in record: + tool_calls_str = json.dumps( + record["tool_calls"], ensure_ascii=False + ) + temp_contexts.append(f"Assistant: [函数调用] {tool_calls_str}") + else: + temp_contexts.append("Assistant: [未知的内容]") contexts.insert(0, temp_contexts) temp_contexts = [] From 5a001871470ba0906eab5457ec1547204d7a5012 Mon Sep 17 00:00:00 2001 From: anka <1350989414@qq.com> Date: Mon, 7 Apr 2025 18:14:30 +0800 Subject: [PATCH 04/31] =?UTF-8?q?fix:=20=E5=AF=B9=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E7=9A=84toolcall=E9=AA=8C=E8=AF=81=E6=98=AF?= =?UTF-8?q?=E5=90=A6=E6=88=90=E5=AF=B9,=20=E5=8F=82=E8=80=83:=20https://gi?= =?UTF-8?q?thub.com/run-llama/llama=5Findex/issues/13715=20https://github.?= =?UTF-8?q?com/run-llama/llama=5Findex/pull/16214?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../process_stage/method/llm_request.py | 63 +++++++++++++++++-- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 987e82570..99b09b5a1 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -64,10 +64,48 @@ class LLMRequestSubStage(Stage): if req.conversation: all_contexts = json.loads(req.conversation.history) - # 对函数工具调用做过滤 - req.contexts = [ - msg for msg in all_contexts if "_tool_call_history" not in msg - ] + req.contexts = [] + i = 0 + while i < len(all_contexts): + current_msg = all_contexts[i] + # 普通消息 + if "_tool_call_history" not in current_msg: + req.contexts.append(current_msg) + i += 1 + continue + + # 工具调用消息, 必须成对出现 + if ( + current_msg.get("role") == "assistant" + and "tool_calls" in current_msg + ): + # 寻找tool响应 + assistant_msg = current_msg.copy() + # 移除标记 + if "_tool_call_history" in assistant_msg: + del assistant_msg["_tool_call_history"] + + related_tools = [] + j = i + 1 + while ( + j < len(all_contexts) + and all_contexts[j].get("role") == "tool" + and "_tool_call_history" in all_contexts[j] + ): + tool_msg = all_contexts[j].copy() + del tool_msg["_tool_call_history"] + related_tools.append(tool_msg) + j += 1 + + # 只添加成对的tool_call和tool响应 + if related_tools: + req.contexts.append(assistant_msg) + req.contexts.extend(related_tools) + # 已处理的消息跳过 + i = j + else: + i += 1 + else: req = ProviderRequest(prompt="", image_urls=[]) if self.provider_wake_prefix: @@ -313,15 +351,28 @@ class LLMRequestSubStage(Stage): if llm_response.role == "assistant": # 文本回复 - contexts = req.contexts + contexts = req.contexts.copy() contexts.append(await req.assemble_context()) # 记录并标记函数调用结果 if req.tool_calls_result: tool_calls_messages = req.tool_calls_result.to_openai_messages() + + # 对顺序的验证 + assistant_msgs = [] + tool_msgs = [] + for message in tool_calls_messages: message["_tool_call_history"] = True - contexts.extend(tool_calls_messages) + + if message.get("role") == "assistant": + assistant_msgs.append(message) + elif message.get("role") == "tool": + tool_msgs.append(message) + + # 先添加assistant再添加tool + contexts.extend(assistant_msgs) + contexts.extend(tool_msgs) contexts.append( {"role": "assistant", "content": llm_response.completion_text} From 43b8414727407e4d464f7f5e1b7c842774ad7a1c Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Mon, 7 Apr 2025 21:51:41 +0800 Subject: [PATCH 05/31] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=8C=87=E4=BB=A4=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/sources/telegram/tg_adapter.py | 113 ++++++++++++++++-- 1 file changed, 101 insertions(+), 12 deletions(-) diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py index 12f17a819..0e706ecbb 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -1,26 +1,31 @@ +import asyncio import sys import uuid -import asyncio -import astrbot.api.message_components as Comp +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from telegram import BotCommand, Update +from telegram.constants import ChatType +from telegram.ext import ApplicationBuilder, ContextTypes, ExtBot, filters +from telegram.ext import MessageHandler as TelegramMessageHandler + +import astrbot.api.message_components as Comp +from astrbot.api import logger +from astrbot.api.event import MessageChain from astrbot.api.platform import ( - Platform, AstrBotMessage, MessageMember, - PlatformMetadata, MessageType, + Platform, + PlatformMetadata, + register_platform_adapter, ) -from astrbot.api.event import MessageChain from astrbot.core.platform.astr_message_event import MessageSesion -from astrbot.api.platform import register_platform_adapter +from astrbot.core.star.filter.command import CommandFilter +from astrbot.core.star.filter.command_group import CommandGroupFilter +from astrbot.core.star.star import star_map +from astrbot.core.star.star_handler import star_handlers_registry -from telegram import Update -from telegram.ext import ApplicationBuilder, ContextTypes, filters -from telegram.constants import ChatType -from telegram.ext import MessageHandler as TelegramMessageHandler from .tg_event import TelegramPlatformEvent -from astrbot.api import logger -from telegram.ext import ExtBot if sys.version_info >= (3, 12): from typing import override @@ -67,6 +72,8 @@ class TelegramPlatformAdapter(Platform): self.client = self.application.bot logger.debug(f"Telegram base url: {self.client.base_url}") + self.scheduler = AsyncIOScheduler() + @override async def send_by_session( self, session: MessageSesion, message_chain: MessageChain @@ -88,10 +95,88 @@ class TelegramPlatformAdapter(Platform): async def run(self): await self.application.initialize() await self.application.start() + await self.register_commands() + + # TODO 使用更优雅的方式重新注册命令 + self.scheduler.add_job( + self.register_commands, + "interval", + minutes=5, + id="telegram_command_register", + misfire_grace_time=60, + ) + self.scheduler.start() + queue = self.application.updater.start_polling() logger.info("Telegram Platform Adapter is running.") await queue + async def register_commands(self): + """收集所有注册的指令并注册到 Telegram""" + try: + await self.client.delete_my_commands() + commands = self.collect_commands() + + if commands: + await self.client.set_my_commands(commands) + for cmd in commands: + logger.debug(f"已注册指令: /{cmd.command} - {cmd.description}") + + except Exception as e: + logger.error(f"向 Telegram 注册指令时发生错误: {e!s}") + + def collect_commands(self) -> list[BotCommand]: + """从注册的处理器中收集所有指令""" + + command_dict = {} + skip_commands = {"start"} + + for handler_md in star_handlers_registry._handlers: + handler_metadata = handler_md[1] + + if not star_map[handler_metadata.handler_module_path].activated: + continue + + for event_filter in handler_metadata.event_filters: + cmd_name = None + is_group = False + + if ( + isinstance(event_filter, CommandFilter) + and event_filter.command_name + ): + if event_filter.parent_command_names: + continue + cmd_name = event_filter.command_name + elif isinstance(event_filter, CommandGroupFilter): + if event_filter.parent_group: + continue + cmd_name = event_filter.group_name + is_group = True + + if ( + cmd_name + and cmd_name not in skip_commands + and cmd_name not in command_dict + ): + if handler_metadata.desc: + description = handler_metadata.desc + else: + description = ( + f"指令组: {cmd_name} (包含多个子指令)" + if is_group + else f"指令: {cmd_name}" + ) + + if len(description) > 30: + description = description[:30] + "..." + + command_dict[cmd_name] = description + + commands_a = sorted(command_dict.keys()) + commands = [BotCommand(cmd, command_dict[cmd]) for cmd in commands_a] + return commands + async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): await context.bot.send_message( chat_id=update.effective_chat.id, text=self.config["start_message"] @@ -242,7 +327,11 @@ class TelegramPlatformAdapter(Platform): async def terminate(self): try: + if self.scheduler.running: + self.scheduler.shutdown() + await self.application.stop() + await self.client.delete_my_commands() # 保险起见先判断是否存在updater对象 if self.application.updater is not None: From 9e04e3679be4016c5495066f8e254415a8b917b2 Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Mon, 7 Apr 2025 22:08:29 +0800 Subject: [PATCH 06/31] =?UTF-8?q?=E4=BF=9D=E8=AF=81=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E6=8C=87=E4=BB=A4=E8=A2=AB=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/platform/sources/telegram/tg_adapter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py index 0e706ecbb..fba96a6fe 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -145,7 +145,10 @@ class TelegramPlatformAdapter(Platform): isinstance(event_filter, CommandFilter) and event_filter.command_name ): - if event_filter.parent_command_names: + if ( + event_filter.parent_command_names + and event_filter.parent_command_names != [""] + ): continue cmd_name = event_filter.command_name elif isinstance(event_filter, CommandGroupFilter): From 735368c71bd5b53359c2a157b7881009802380d3 Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Mon, 7 Apr 2025 22:16:02 +0800 Subject: [PATCH 07/31] =?UTF-8?q?=E4=BF=9D=E8=AF=81=E5=8F=98=E9=87=8F?= =?UTF-8?q?=E5=90=8D=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/platform/sources/telegram/tg_adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py index fba96a6fe..2852df70e 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -176,8 +176,8 @@ class TelegramPlatformAdapter(Platform): command_dict[cmd_name] = description - commands_a = sorted(command_dict.keys()) - commands = [BotCommand(cmd, command_dict[cmd]) for cmd in commands_a] + sorted_commands = sorted(command_dict.keys()) + commands = [BotCommand(cmd, command_dict[cmd]) for cmd in sorted_commands] return commands async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): From db257af58efb5fa0a66e9403552b0462bc58e8db Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Mon, 7 Apr 2025 22:29:50 +0800 Subject: [PATCH 08/31] =?UTF-8?q?=E6=8F=90=E5=8D=87=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/sources/telegram/tg_adapter.py | 76 +++++++++---------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py index 2852df70e..332112880 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -127,58 +127,54 @@ class TelegramPlatformAdapter(Platform): def collect_commands(self) -> list[BotCommand]: """从注册的处理器中收集所有指令""" - command_dict = {} skip_commands = {"start"} for handler_md in star_handlers_registry._handlers: handler_metadata = handler_md[1] - if not star_map[handler_metadata.handler_module_path].activated: continue - for event_filter in handler_metadata.event_filters: - cmd_name = None - is_group = False + cmd_info = self._extract_command_info( + event_filter, handler_metadata, skip_commands + ) + if cmd_info: + cmd_name, description = cmd_info + command_dict.setdefault(cmd_name, description) - if ( - isinstance(event_filter, CommandFilter) - and event_filter.command_name - ): - if ( - event_filter.parent_command_names - and event_filter.parent_command_names != [""] - ): - continue - cmd_name = event_filter.command_name - elif isinstance(event_filter, CommandGroupFilter): - if event_filter.parent_group: - continue - cmd_name = event_filter.group_name - is_group = True + commands_a = sorted(command_dict.keys()) + return [BotCommand(cmd, command_dict[cmd]) for cmd in commands_a] - if ( - cmd_name - and cmd_name not in skip_commands - and cmd_name not in command_dict - ): - if handler_metadata.desc: - description = handler_metadata.desc - else: - description = ( - f"指令组: {cmd_name} (包含多个子指令)" - if is_group - else f"指令: {cmd_name}" - ) + @staticmethod + def _extract_command_info( + event_filter, handler_metadata, skip_commands: set + ) -> tuple[str, str] | None: + """从事件过滤器中提取指令信息""" + cmd_name = None + is_group = False + if isinstance(event_filter, CommandFilter) and event_filter.command_name: + if ( + event_filter.parent_command_names + and event_filter.parent_command_names != [""] + ): + return None + cmd_name = event_filter.command_name + elif isinstance(event_filter, CommandGroupFilter): + if event_filter.parent_group: + return None + cmd_name = event_filter.group_name + is_group = True - if len(description) > 30: - description = description[:30] + "..." + if not cmd_name or cmd_name in skip_commands: + return None - command_dict[cmd_name] = description - - sorted_commands = sorted(command_dict.keys()) - commands = [BotCommand(cmd, command_dict[cmd]) for cmd in sorted_commands] - return commands + # Build description. + description = handler_metadata.desc or ( + f"指令组: {cmd_name} (包含多个子指令)" if is_group else f"指令: {cmd_name}" + ) + if len(description) > 30: + description = description[:30] + "..." + return cmd_name, description async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): await context.bot.send_message( From ae4c6fe2ddf9450a9fa433c4952bdf09126f46ba Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Tue, 8 Apr 2025 10:41:47 +0800 Subject: [PATCH 09/31] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=8C=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E5=AE=8C=E6=95=B4=E5=A4=84=E7=90=86=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E6=89=80=E6=9C=89=E6=A8=A1=E5=9D=97=E3=80=82=E4=B8=BA=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E6=96=B9=E6=B3=95=E6=B7=BB=E5=8A=A0=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/star/star_manager.py | 146 +++++++++++++++++++++++++++--- 1 file changed, 131 insertions(+), 15 deletions(-) diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 2d610f15c..c0a3aa38b 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -166,8 +166,42 @@ class PluginManager: return metadata + def _get_plugin_related_modules( + self, plugin_root_dir: str, is_reserved: bool = False + ) -> list[str]: + """获取插件相关的所有模块名 + Args: + plugin_root_dir: 插件根目录名 + is_reserved: 是否是保留插件 + Returns: + 模块名列表 + """ + prefix = "packages." if is_reserved else "data.plugins." + return [ + key + for key in list(sys.modules.keys()) + if key.startswith(f"{prefix}{plugin_root_dir}") + ] + async def reload(self, specified_plugin_name=None): - """扫描并加载所有的插件 当 specified_module_path 指定时,重载指定插件""" + """重新加载插件 + + Args: + specified_plugin_name (str, optional): 要重载的特定插件名称。 + 如果为 None,则重载所有插件。 + + Returns: + tuple: 返回 load() 方法的结果,包含 (success, error_message) + - success (bool): 重载是否成功 + - error_message (str|None): 错误信息,成功时为 None + + 流程: + 1. 如果指定了插件名,查找对应的模块路径 + 2. 终止现有插件实例 + 3. 解绑插件相关的处理器和注册信息 + 4. 清理系统模块缓存 + 5. 重新加载插件 + """ specified_module_path = None if specified_plugin_name: for smd in star_registry: @@ -209,11 +243,45 @@ class PluginManager: await self._unbind_plugin(smd.name, specified_module_path) + for module_name in self._get_plugin_related_modules( + smd.root_dir_name, smd.reserved + ): + try: + del sys.modules[module_name] + except KeyError: + pass + return await self.load(specified_module_path) async def load(self, specified_module_path=None, specified_dir_name=None): """载入插件。 当 specified_module_path 或者 specified_dir_name 不为 None 时,只载入指定的插件。 + + Args: + specified_module_path (str, optional): 指定要加载的插件模块路径。例如: "data.plugins.my_plugin.main" + specified_dir_name (str, optional): 指定要加载的插件目录名。例如: "my_plugin" + + Returns: + tuple: (success, error_message) + - success (bool): 是否全部加载成功 + - error_message (str|None): 错误信息,成功时为 None + + 执行步骤: + 1. 获取已禁用的插件和 LLM 工具列表 + 2. 获取所有插件模块信息 + 3. 对每个插件模块: + - 导入模块并实例化插件类 + - 加载插件配置(如果存在 _conf_schema.json) + - 处理插件元数据(metadata) + - 绑定事件处理器和 LLM 工具 + - 执行插件的 initialize() 方法 + 4. 记录加载失败的插件信息 + 5. 清理 pip.main 产生的日志处理器 + + 注意: + - 插件可以通过装饰器方式注册(v3.4.0+)或旧版本方式注册 + - 禁用的插件不会被实例化 + - 支持自定义命令权限过滤 """ inactivated_plugins: list = sp.get("inactivated_plugins", []) inactivated_llm_tools: list = sp.get("inactivated_llm_tools", []) @@ -447,6 +515,25 @@ class PluginManager: return False, fail_rec async def install_plugin(self, repo_url: str, proxy=""): + """从仓库 URL 安装插件。 + + 参数: + repo_url (str): 要安装的插件仓库 URL + proxy (str, optional): 用于下载的代理服务器。默认为空字符串。 + + 返回: + dict | None: 安装成功时返回包含插件信息的字典: + - repo: 插件的仓库 URL + - readme: README.md 文件的内容(如果存在) + 如果找不到插件元数据则返回 None。 + + 函数执行步骤: + 1. 从仓库下载并安装插件 + 2. 将插件重新加载到系统中 + 3. 尝试使用目录名称查找插件元数据 + 4. 提取 README.md 内容(如果可用) + 5. 返回仓库和说明文档信息 + """ plugin_path = await self.updator.install(repo_url, proxy) # reload the plugin dir_name = os.path.basename(plugin_path) @@ -481,6 +568,20 @@ class PluginManager: return plugin_info async def uninstall_plugin(self, plugin_name: str): + """卸载指定的插件。 + + Args: + plugin_name (str): 要卸载的插件名称 + + Raises: + Exception: 当插件不存在、是保留插件时,或删除插件文件夹失败时抛出异常 + + 执行以下步骤: + 1. 检查插件是否存在且不是保留插件 + 2. 调用插件的终止方法 + 3. 从注册表中解绑插件 + 4. 删除插件文件夹 + """ plugin = self.context.get_registered_star(plugin_name) if not plugin: raise Exception("插件不存在。") @@ -509,9 +610,23 @@ class PluginManager: ) async def _unbind_plugin(self, plugin_name: str, plugin_module_path: str): + """解绑并移除一个插件。 + + Args: + plugin_name: 要解绑的插件名称 + plugin_module_path: 插件的完整模块路径 + + 该方法会执行以下操作: + 1. 从 star_map 和 star_registry 中移除插件 + 2. 移除该插件注册的所有处理函数 + 3. 清理处理函数映射 + 4. 从 sys.modules 中卸载所有相关的插件模块 + """ + plugin = None del star_map[plugin_module_path] for i, p in enumerate(star_registry): if p.name == plugin_name: + plugin = p del star_registry[i] break for handler in star_handlers_registry.get_handlers_by_module_name( @@ -521,21 +636,22 @@ class PluginManager: f"移除了插件 {plugin_name} 的处理函数 {handler.handler_name} ({len(star_handlers_registry)})" ) star_handlers_registry.remove(handler) - keys_to_delete = [ - k - for k, v in star_handlers_registry.star_handlers_map.items() - if k.startswith(plugin_module_path) - ] - for k in keys_to_delete: - try: - del star_handlers_registry.star_handlers_map[k] - except KeyError: - pass - try: - del sys.modules[plugin_module_path] - except KeyError: - logger.warning(f"模块 {plugin_module_path} 未载入") + for k in [ + k + for k in star_handlers_registry.star_handlers_map + if k.startswith(plugin_module_path) + ]: + del star_handlers_registry.star_handlers_map[k] + + for module_name in self._get_plugin_related_modules( + plugin.root_dir_name, plugin.reserved + ): + try: + del sys.modules[module_name] + logger.debug(f"删除模块 {module_name}") + except KeyError: + logger.warning(f"模块 {module_name} 未载入") async def update_plugin(self, plugin_name: str, proxy=""): """升级一个插件""" From 7cd1eeac309417149035a99d9e9595ec2aa3a27d Mon Sep 17 00:00:00 2001 From: anka <1350989414@qq.com> Date: Tue, 8 Apr 2025 15:57:38 +0000 Subject: [PATCH 10/31] =?UTF-8?q?fix:=20=E7=9B=B4=E6=8E=A5=E6=8A=8A?= =?UTF-8?q?=E7=A9=BA=E5=AD=97=E7=AC=A6=E4=B8=B2=E6=94=B9=E4=B8=BA"=20"?= =?UTF-8?q?=E4=B8=80=E6=9D=A1=E6=B6=88=E6=81=AF=E7=9A=84content=E6=98=AF?= =?UTF-8?q?=E7=A9=BA=E5=AD=97=E7=AC=A6=E4=B8=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/provider/sources/gemini_source.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index 7ae418938..11f3f7eaa 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -146,10 +146,12 @@ class ProviderGoogleGenAI(Provider): for message in payloads["messages"]: if message["role"] == "user": if isinstance(message["content"], str): - if message["content"]: - google_genai_conversation.append( - {"role": "user", "parts": [{"text": message["content"]}]} - ) + if not message["content"]: + message["content"] = " " + + google_genai_conversation.append( + {"role": "user", "parts": [{"text": message["content"]}]} + ) elif isinstance(message["content"], list): # images parts = [] @@ -173,10 +175,11 @@ class ProviderGoogleGenAI(Provider): elif message["role"] == "assistant": if "content" in message: - if message["content"]: - google_genai_conversation.append( - {"role": "model", "parts": [{"text": message["content"]}]} - ) + if not message["content"]: + message["content"] = " " + google_genai_conversation.append( + {"role": "model", "parts": [{"text": message["content"]}]} + ) elif "tool_calls" in message: # tool calls in the last turn parts = [] From b62b1f38704ffdd6ee1f166dd00260e898656c6e Mon Sep 17 00:00:00 2001 From: zhx Date: Mon, 7 Apr 2025 20:27:41 +0800 Subject: [PATCH 11/31] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E8=83=BD=E9=92=88=E5=AF=B9=E4=B8=8D=E5=90=8C=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=B9=B3=E5=8F=B0=E5=BC=80=E5=90=AF=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed: chore: merge master branch chore: merge from master branch chore: rename updateAllPlatformCompatibility to update_all_platform_compatibility for consistency Reviewed by: @Raven95676 @Soulter --- astrbot/core/pipeline/__init__.py | 3 + .../pipeline/platform_compatibility/stage.py | 52 +++++ .../process_stage/method/llm_request.py | 15 +- .../process_stage/method/star_request.py | 11 + astrbot/core/pipeline/respond/stage.py | 2 +- .../core/pipeline/result_decorate/stage.py | 2 +- astrbot/core/platform/astr_message_event.py | 3 + astrbot/core/platform/platform_metadata.py | 2 + .../aiocqhttp/aiocqhttp_platform_adapter.py | 5 +- .../sources/dingtalk/dingtalk_adapter.py | 5 +- .../gewechat/gewechat_platform_adapter.py | 5 +- .../platform/sources/lark/lark_adapter.py | 5 +- .../qqofficial/qqofficial_platform_adapter.py | 5 +- .../qqofficial_webhook/qo_webhook_adapter.py | 5 +- .../platform/sources/telegram/tg_adapter.py | 3 +- .../sources/webchat/webchat_adapter.py | 3 +- astrbot/core/star/filter/command.py | 0 astrbot/core/star/filter/command_group.py | 0 astrbot/core/star/star.py | 24 ++ astrbot/core/star/star_handler.py | 72 ++++-- astrbot/core/star/star_manager.py | 32 ++- astrbot/dashboard/routes/plugin.py | 119 +++++++++- dashboard/src/views/ExtensionPage.vue | 216 +++++++++++++++++- 23 files changed, 542 insertions(+), 47 deletions(-) create mode 100644 astrbot/core/pipeline/platform_compatibility/stage.py mode change 100644 => 100755 astrbot/core/star/filter/command.py mode change 100644 => 100755 astrbot/core/star/filter/command_group.py diff --git a/astrbot/core/pipeline/__init__.py b/astrbot/core/pipeline/__init__.py index b97fc0f12..406fcc796 100644 --- a/astrbot/core/pipeline/__init__.py +++ b/astrbot/core/pipeline/__init__.py @@ -7,6 +7,7 @@ from .waking_check.stage import WakingCheckStage from .whitelist_check.stage import WhitelistCheckStage from .rate_limit_check.stage import RateLimitStage from .content_safety_check.stage import ContentSafetyCheckStage +from .platform_compatibility.stage import PlatformCompatibilityStage from .preprocess_stage.stage import PreProcessStage from .process_stage.stage import ProcessStage from .result_decorate.stage import ResultDecorateStage @@ -18,6 +19,7 @@ STAGES_ORDER = [ "WhitelistCheckStage", # 检查是否在群聊/私聊白名单 "RateLimitStage", # 检查会话是否超过频率限制 "ContentSafetyCheckStage", # 检查内容安全 + "PlatformCompatibilityStage", # 检查所有处理器的平台兼容性 "PreProcessStage", # 预处理 "ProcessStage", # 交由 Stars 处理(a.k.a 插件),或者 LLM 调用 "ResultDecorateStage", # 处理结果,比如添加回复前缀、t2i、转换为语音 等 @@ -29,6 +31,7 @@ __all__ = [ "WhitelistCheckStage", "RateLimitStage", "ContentSafetyCheckStage", + "PlatformCompatibilityStage", "PreProcessStage", "ProcessStage", "ResultDecorateStage", diff --git a/astrbot/core/pipeline/platform_compatibility/stage.py b/astrbot/core/pipeline/platform_compatibility/stage.py new file mode 100644 index 000000000..0b0b04fda --- /dev/null +++ b/astrbot/core/pipeline/platform_compatibility/stage.py @@ -0,0 +1,52 @@ +from ..stage import Stage, register_stage +from ..context import PipelineContext +from typing import Union, AsyncGenerator +from astrbot.core.platform.astr_message_event import AstrMessageEvent +from astrbot.core.star.star import star_map +from astrbot.core import logger + + +@register_stage +class PlatformCompatibilityStage(Stage): + """检查所有处理器的平台兼容性。 + + 这个阶段会检查所有处理器是否在当前平台启用,如果未启用则设置platform_compatible属性为False。 + """ + + async def initialize(self, ctx: PipelineContext) -> None: + """初始化平台兼容性检查阶段 + + Args: + ctx (PipelineContext): 消息管道上下文对象, 包括配置和插件管理器 + """ + self.ctx = ctx + + async def process( + self, event: AstrMessageEvent + ) -> Union[None, AsyncGenerator[None, None]]: + # 获取当前平台ID + platform_id = event.get_platform_id() + + # 获取已激活的处理器 + activated_handlers = event.get_extra("activated_handlers") + if activated_handlers is None: + activated_handlers = [] + + # 标记不兼容的处理器 + for handler in activated_handlers: + # 检查处理器是否在当前平台启用 + enabled = handler.is_enabled_for_platform(platform_id) + if not enabled: + if handler.handler_module_path in star_map: + plugin_name = star_map[handler.handler_module_path].name + logger.debug( + f"[PlatformCompatibilityStage] 插件 {plugin_name} 在平台 {platform_id} 未启用,标记处理器 {handler.handler_name} 为平台不兼容" + ) + # 设置处理器为平台不兼容状态 + handler.platform_compatible = False + else: + # 确保处理器为平台兼容状态 + handler.platform_compatible = True + + # 更新已激活的处理器列表 + event.set_extra("activated_handlers", activated_handlers) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index c6a87b37c..3a129ea80 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -108,8 +108,10 @@ class LLMRequestSubStage(Stage): # 执行请求 LLM 前事件钩子。 # 装饰 system_prompt 等功能 + # 获取当前平台ID + platform_id = event.get_platform_id() handlers = star_handlers_registry.get_handlers_by_event_type( - EventType.OnLLMRequestEvent + EventType.OnLLMRequestEvent, platform_id=platform_id ) for handler in handlers: try: @@ -350,6 +352,8 @@ class LLMRequestSubStage(Stage): llm_response.tools_call_args, llm_response.tools_call_ids, ): + + try: func_tool = req.func_tool.get_func(func_tool_name) if func_tool.origin == "mcp": @@ -368,6 +372,15 @@ class LLMRequestSubStage(Stage): ) ) else: + # 获取处理器,过滤掉平台不兼容的处理器 + platform_id = event.get_platform_id() + if not func_tool.handler.is_enabled_for_platform(platform_id): + logger.debug( + f"处理器 {func_tool_name} 在当前平台不兼容,跳过执行" + ) + # 直接跳过,不添加任何消息到tool_call_result + continue + logger.info( f"调用工具函数:{func_tool_name},参数:{func_tool_args}" ) diff --git a/astrbot/core/pipeline/process_stage/method/star_request.py b/astrbot/core/pipeline/process_stage/method/star_request.py index d369e53ed..c7817e49c 100644 --- a/astrbot/core/pipeline/process_stage/method/star_request.py +++ b/astrbot/core/pipeline/process_stage/method/star_request.py @@ -31,7 +31,18 @@ class StarRequestSubStage(Stage): ) if not handlers_parsed_params: handlers_parsed_params = {} + for handler in activated_handlers: + # 检查处理器是否在当前平台兼容 + if ( + hasattr(handler, "platform_compatible") + and handler.platform_compatible is False + ): + logger.debug( + f"处理器 {handler.handler_name} 在当前平台不兼容,跳过执行" + ) + continue + params = handlers_parsed_params.get(handler.handler_full_name, {}) try: if handler.handler_module_path not in star_map: diff --git a/astrbot/core/pipeline/respond/stage.py b/astrbot/core/pipeline/respond/stage.py index 0d5044054..60a052454 100644 --- a/astrbot/core/pipeline/respond/stage.py +++ b/astrbot/core/pipeline/respond/stage.py @@ -198,7 +198,7 @@ class RespondStage(Stage): ) handlers = star_handlers_registry.get_handlers_by_event_type( - EventType.OnAfterMessageSentEvent + EventType.OnAfterMessageSentEvent, platform_id=event.get_platform_id() ) for handler in handlers: try: diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index a0be2423b..957e2a491 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -96,7 +96,7 @@ class ResultDecorateStage(Stage): # 发送消息前事件钩子 handlers = star_handlers_registry.get_handlers_by_event_type( - EventType.OnDecoratingResultEvent + EventType.OnDecoratingResultEvent, platform_id=event.get_platform_id() ) for handler in handlers: try: diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py index 8d3bc4c59..96a7ad6f1 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -81,6 +81,9 @@ class AstrMessageEvent(abc.ABC): def get_platform_name(self): return self.platform_meta.name + def get_platform_id(self): + return self.platform_meta.id + def get_message_str(self) -> str: """ 获取消息字符串。 diff --git a/astrbot/core/platform/platform_metadata.py b/astrbot/core/platform/platform_metadata.py index 48fe23af7..dd0e93fec 100644 --- a/astrbot/core/platform/platform_metadata.py +++ b/astrbot/core/platform/platform_metadata.py @@ -7,6 +7,8 @@ class PlatformMetadata: """平台的名称""" description: str """平台的描述""" + id: str = None + """平台的唯一标识符,用于配置中识别特定平台""" default_config_tmpl: dict = None """平台的默认配置模板""" diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index e41071a56..88f2ae3fc 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -39,8 +39,9 @@ class AiocqhttpAdapter(Platform): self.port = platform_config["ws_reverse_port"] self.metadata = PlatformMetadata( - "aiocqhttp", - "适用于 OneBot 标准的消息平台适配器,支持反向 WebSockets。", + name="aiocqhttp", + description="适用于 OneBot 标准的消息平台适配器,支持反向 WebSockets。", + id=self.config.get("id"), ) self.bot = CQHttp( diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py index 95347172b..7a83a8abe 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py @@ -73,8 +73,9 @@ class DingtalkPlatformAdapter(Platform): def meta(self) -> PlatformMetadata: return PlatformMetadata( - "dingtalk", - "钉钉机器人官方 API 适配器", + name="dingtalk", + description="钉钉机器人官方 API 适配器", + id=self.config.get("id"), ) async def convert_msg( diff --git a/astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py b/astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py index acf39197f..930359837 100644 --- a/astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +++ b/astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py @@ -60,8 +60,9 @@ class GewechatPlatformAdapter(Platform): @override def meta(self) -> PlatformMetadata: return PlatformMetadata( - "gewechat", - "基于 gewechat 的 Wechat 适配器", + name="gewechat", + description="基于 gewechat 的 Wechat 适配器", + id=self.config.get("id"), ) async def terminate(self): diff --git a/astrbot/core/platform/sources/lark/lark_adapter.py b/astrbot/core/platform/sources/lark/lark_adapter.py index cbc3a45bb..8ea2ce36b 100644 --- a/astrbot/core/platform/sources/lark/lark_adapter.py +++ b/astrbot/core/platform/sources/lark/lark_adapter.py @@ -70,8 +70,9 @@ class LarkPlatformAdapter(Platform): def meta(self) -> PlatformMetadata: return PlatformMetadata( - "lark", - "飞书机器人官方 API 适配器", + name="lark", + description="飞书机器人官方 API 适配器", + id=self.config.get("id"), ) async def convert_msg(self, event: lark.im.v1.P2ImMessageReceiveV1): diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py index 57bc8683f..d5285f759 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py @@ -126,8 +126,9 @@ class QQOfficialPlatformAdapter(Platform): def meta(self) -> PlatformMetadata: return PlatformMetadata( - "qq_official", - "QQ 机器人官方 API 适配器", + name="qq_official", + description="QQ 机器人官方 API 适配器", + id=self.config.get("id"), ) @staticmethod diff --git a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py index ede09e7fd..cc12e9765 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py @@ -99,8 +99,9 @@ class QQOfficialWebhookPlatformAdapter(Platform): def meta(self) -> PlatformMetadata: return PlatformMetadata( - "qq_official_webhook", - "QQ 机器人官方 API 适配器", + name="qq_official_webhook", + description="QQ 机器人官方 API 适配器", + id=self.config.get("id"), ) async def run(self): diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py index 12f17a819..9ff761c06 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -80,8 +80,7 @@ class TelegramPlatformAdapter(Platform): @override def meta(self) -> PlatformMetadata: return PlatformMetadata( - "telegram", - "telegram 适配器", + name="telegram", description="telegram 适配器", id=self.config.get("id") ) @override diff --git a/astrbot/core/platform/sources/webchat/webchat_adapter.py b/astrbot/core/platform/sources/webchat/webchat_adapter.py index 6fa3d5c59..01a042fb8 100644 --- a/astrbot/core/platform/sources/webchat/webchat_adapter.py +++ b/astrbot/core/platform/sources/webchat/webchat_adapter.py @@ -43,8 +43,7 @@ class WebChatAdapter(Platform): self.imgs_dir = "data/webchat/imgs" self.metadata = PlatformMetadata( - "webchat", - "webchat", + name="webchat", description="webchat", id=self.config.get("id") ) async def send_by_session( diff --git a/astrbot/core/star/filter/command.py b/astrbot/core/star/filter/command.py old mode 100644 new mode 100755 diff --git a/astrbot/core/star/filter/command_group.py b/astrbot/core/star/filter/command_group.py old mode 100644 new mode 100755 diff --git a/astrbot/core/star/star.py b/astrbot/core/star/star.py index 521513449..10cf90c8b 100644 --- a/astrbot/core/star/star.py +++ b/astrbot/core/star/star.py @@ -47,5 +47,29 @@ class StarMetadata: star_handler_full_names: List[str] = field(default_factory=list) """注册的 Handler 的全名列表""" + supported_platforms: Dict[str, bool] = field(default_factory=dict) + """插件支持的平台ID字典,key为平台ID,value为是否支持""" + def __str__(self) -> str: return f"StarMetadata({self.name}, {self.desc}, {self.version}, {self.repo})" + + def update_platform_compatibility(self, plugin_enable_config: dict) -> None: + """更新插件支持的平台列表 + + Args: + plugin_enable_config: 平台插件启用配置,即platform_settings.plugin_enable配置项 + """ + if not plugin_enable_config: + return + + # 清空之前的配置 + self.supported_platforms.clear() + + # 遍历所有平台配置 + for platform_id, plugins in plugin_enable_config.items(): + # 检查该插件在当前平台的配置 + if self.name in plugins: + self.supported_platforms[platform_id] = plugins[self.name] + else: + # 如果没有明确配置,默认为启用 + self.supported_platforms[platform_id] = True diff --git a/astrbot/core/star/star_handler.py b/astrbot/core/star/star_handler.py index 7be0e053c..0764f15f6 100644 --- a/astrbot/core/star/star_handler.py +++ b/astrbot/core/star/star_handler.py @@ -30,21 +30,36 @@ class StarHandlerRegistry(Generic[T]): print(handler.handler_full_name) def get_handlers_by_event_type( - self, event_type: EventType, only_activated=True + self, event_type: EventType, only_activated=True, platform_id=None ) -> List[StarHandlerMetadata]: - """通过事件类型获取 Handler""" - handlers = [ - handler - for _, handler in self._handlers - if handler.event_type == event_type - and ( - not only_activated - or ( - star_map[handler.handler_module_path] - and star_map[handler.handler_module_path].activated - ) - ) - ] + """通过事件类型获取 Handler + + Args: + event_type: 事件类型 + only_activated: 是否只返回已激活的插件的处理器 + platform_id: 平台ID,如果提供此参数,将过滤掉在此平台不兼容的处理器 + + Returns: + List[StarHandlerMetadata]: 处理器列表 + """ + handlers = [] + for _, handler in self._handlers: + if handler.event_type != event_type: + continue + + # 只激活的插件处理器 + if only_activated: + plugin = star_map.get(handler.handler_module_path) + if not (plugin and plugin.activated): + continue + + # 平台兼容性过滤 + if platform_id and event_type != EventType.OnAstrBotLoadedEvent: + if not handler.is_enabled_for_platform(platform_id): + continue + + handlers.append(handler) + return handlers def get_handler_by_full_name(self, full_name: str) -> StarHandlerMetadata: @@ -139,3 +154,32 @@ class StarHandlerMetadata: return self.extras_configs.get("priority", 0) < other.extras_configs.get( "priority", 0 ) + + def is_enabled_for_platform(self, platform_id: str) -> bool: + """检查插件是否在指定平台启用 + + Args: + platform_id: 平台ID,这是从event.get_platform_id()获取的,用于唯一标识平台实例 + + Returns: + bool: 是否启用,True表示启用,False表示禁用 + """ + plugin = star_map.get(self.handler_module_path) + + # 如果插件元数据不存在,默认允许执行 + if not plugin or not plugin.name: + return True + + # 先检查插件是否被激活 + if not plugin.activated: + return False + + # 直接使用StarMetadata中缓存的supported_platforms判断平台兼容性 + if ( + hasattr(plugin, "supported_platforms") + and platform_id in plugin.supported_platforms + ): + return plugin.supported_platforms[platform_id] + + # 如果没有缓存数据,默认允许执行 + return True diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 2d610f15c..a4ae48250 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -209,7 +209,31 @@ class PluginManager: await self._unbind_plugin(smd.name, specified_module_path) - return await self.load(specified_module_path) + result = await self.load(specified_module_path) + + # 更新所有插件的平台兼容性 + await self.update_all_platform_compatibility() + + return result + + async def update_all_platform_compatibility(self): + """更新所有插件的平台兼容性设置""" + # 获取最新的平台插件启用配置 + plugin_enable_config = self.config.get("platform_settings", {}).get( + "plugin_enable", {} + ) + logger.debug( + f"更新所有插件的平台兼容性设置,平台数量: {len(plugin_enable_config)}" + ) + + # 遍历所有插件,更新平台兼容性 + for plugin in self.context.get_all_stars(): + plugin.update_platform_compatibility(plugin_enable_config) + logger.debug( + f"插件 {plugin.name} 支持的平台: {list(plugin.supported_platforms.keys())}" + ) + + return True async def load(self, specified_module_path=None, specified_dir_name=None): """载入插件。 @@ -320,6 +344,12 @@ class PluginManager: metadata.root_dir_name = root_dir_name metadata.reserved = reserved + # 更新插件的平台兼容性 + plugin_enable_config = self.config.get("platform_settings", {}).get( + "plugin_enable", {} + ) + metadata.update_platform_compatibility(plugin_enable_config) + # 绑定 handler related_handlers = ( star_handlers_registry.get_handlers_by_module_name( diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index 81d9b0bfe..9fb9d231a 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -38,6 +38,8 @@ class PluginRoute(Route): "/plugin/on": ("POST", self.on_plugin), "/plugin/reload": ("POST", self.reload_plugins), "/plugin/readme": ("GET", self.get_plugin_readme), + "/plugin/platform_enable/get": ("GET", self.get_plugin_platform_enable), + "/plugin/platform_enable/set": ("POST", self.set_plugin_platform_enable), } self.core_lifecycle = core_lifecycle self.plugin_manager = plugin_manager @@ -323,38 +325,131 @@ class PluginRoute(Route): async def get_plugin_readme(self): plugin_name = request.args.get("name") logger.debug(f"正在获取插件 {plugin_name} 的README文件内容") - + if not plugin_name: logger.warning("插件名称为空") return Response().error("插件名称不能为空").__dict__ - + plugin_obj = None for plugin in self.plugin_manager.context.get_all_stars(): if plugin.name == plugin_name: plugin_obj = plugin break - + if not plugin_obj: logger.warning(f"插件 {plugin_name} 不存在") return Response().error(f"插件 {plugin_name} 不存在").__dict__ - - plugin_dir = os.path.join(self.plugin_manager.plugin_store_path, plugin_obj.root_dir_name) - + + plugin_dir = os.path.join( + self.plugin_manager.plugin_store_path, plugin_obj.root_dir_name + ) + if not os.path.isdir(plugin_dir): logger.warning(f"无法找到插件目录: {plugin_dir}") return Response().error(f"无法找到插件 {plugin_name} 的目录").__dict__ - + readme_path = os.path.join(plugin_dir, "README.md") - + if not os.path.isfile(readme_path): logger.warning(f"插件 {plugin_name} 没有README文件") return Response().error(f"插件 {plugin_name} 没有README文件").__dict__ - + try: - with open(readme_path, 'r', encoding='utf-8') as f: + with open(readme_path, "r", encoding="utf-8") as f: readme_content = f.read() - - return Response().ok({"content": readme_content}, "成功获取README内容").__dict__ + + return ( + Response() + .ok({"content": readme_content}, "成功获取README内容") + .__dict__ + ) except Exception as e: logger.error(f"/api/plugin/readme: {traceback.format_exc()}") return Response().error(f"读取README文件失败: {str(e)}").__dict__ + + async def get_plugin_platform_enable(self): + """获取插件在各平台的可用性配置""" + try: + platform_enable = self.core_lifecycle.astrbot_config.get( + "platform_settings", {} + ).get("plugin_enable", {}) + + # 获取所有可用平台 + platforms = [] + + for platform in self.core_lifecycle.astrbot_config.get("platform", []): + platform_type = platform.get("type", "") + platform_id = platform.get("id", "") + + platforms.append( + { + "name": platform_id, # 使用type作为name,这是系统内部使用的平台名称 + "id": platform_id, # 保留id字段以便前端可以显示 + "type": platform_type, + "display_name": f"{platform_type}({platform_id})", + } + ) + + adjusted_platform_enable = {} + for platform_id, plugins in platform_enable.items(): + adjusted_platform_enable[platform_id] = plugins + + # 获取所有插件,包括系统内部插件 + plugins = [] + for plugin in self.plugin_manager.context.get_all_stars(): + plugins.append( + { + "name": plugin.name, + "desc": plugin.desc, + "reserved": plugin.reserved, # 添加reserved标志 + } + ) + + logger.debug( + f"获取插件平台配置: 原始配置={platform_enable}, 调整后={adjusted_platform_enable}" + ) + + return ( + Response() + .ok( + { + "platforms": platforms, + "plugins": plugins, + "platform_enable": adjusted_platform_enable, + } + ) + .__dict__ + ) + except Exception as e: + logger.error(f"/api/plugin/platform_enable/get: {traceback.format_exc()}") + return Response().error(str(e)).__dict__ + + async def set_plugin_platform_enable(self): + """设置插件在各平台的可用性配置""" + if DEMO_MODE: + return ( + Response() + .error("You are not permitted to do this operation in demo mode") + .__dict__ + ) + + try: + data = await request.json + platform_enable = data.get("platform_enable", {}) + + # 更新配置 + config = self.core_lifecycle.astrbot_config + platform_settings = config.get("platform_settings", {}) + platform_settings["plugin_enable"] = platform_enable + config["platform_settings"] = platform_settings + config.save_config() + + # 更新插件的平台兼容性缓存 + await self.plugin_manager.update_all_platform_compatibility() + + logger.info(f"插件平台可用性配置已更新: {platform_enable}") + + return Response().ok(None, "插件平台可用性配置已更新").__dict__ + except Exception as e: + logger.error(f"/api/plugin/platform_enable/set: {traceback.format_exc()}") + return Response().error(str(e)).__dict__ diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index 33f78dd01..2f34deccf 100644 --- a/dashboard/src/views/ExtensionPage.vue +++ b/dashboard/src/views/ExtensionPage.vue @@ -41,6 +41,14 @@ const readmeDialog = reactive({ pluginName: '', repoUrl: null }); +// 平台插件配置 +const platformEnableDialog = ref(false); +const platformEnableData = reactive({ + platforms: [], + plugins: [], + platform_enable: {} +}); +const loadingPlatformData = ref(false); const plugin_handler_info_headers = [ { title: '行为类型', key: 'event_type_h' }, @@ -238,6 +246,101 @@ const viewReadme = (plugin) => { readmeDialog.show = true; }; +// 获取插件平台可用性配置 +const getPlatformEnableConfig = async () => { + loadingPlatformData.value = true; + try { + const res = await axios.get('/api/plugin/platform_enable/get'); + if (res.data.status === "error") { + toast(res.data.message, "error"); + return; + } + + platformEnableData.platforms = res.data.data.platforms; + platformEnableData.plugins = res.data.data.plugins; + platformEnableData.platform_enable = res.data.data.platform_enable; + + // 如果没有平台,给出提示但仍显示对话框 + if (platformEnableData.platforms.length === 0) { + toast("未添加任何平台适配器,请先在平台管理中添加平台", "warning"); + } else { + // 确保每个平台都有一个配置对象 + platformEnableData.platforms.forEach(platform => { + if (!platformEnableData.platform_enable[platform.name]) { + platformEnableData.platform_enable[platform.name] = {}; + } + + // 确保每个插件在每个平台都有一个配置项 + platformEnableData.plugins.forEach(plugin => { + if (platformEnableData.platform_enable[platform.name][plugin.name] === undefined) { + platformEnableData.platform_enable[platform.name][plugin.name] = true; // 默认启用 + } + }); + }); + } + + platformEnableDialog.value = true; + } catch (err) { + toast("获取平台插件配置失败: " + err, "error"); + } finally { + loadingPlatformData.value = false; + } +}; + +// 保存插件平台可用性配置 +const savePlatformEnableConfig = async () => { + loadingPlatformData.value = true; + try { + const res = await axios.post('/api/plugin/platform_enable/set', { + platform_enable: platformEnableData.platform_enable + }); + + if (res.data.status === "error") { + toast(res.data.message, "error"); + return; + } + + toast(res.data.message, "success"); + platformEnableDialog.value = false; + } catch (err) { + toast("保存平台插件配置失败: " + err, "error"); + } finally { + loadingPlatformData.value = false; + } +}; + +// 全选指定平台的所有插件 +const selectAllPluginsForPlatform = (platformName, isSelected, onlyReserved = null) => { + // 确保平台存在于platform_enable中 + if (!platformEnableData.platform_enable[platformName]) { + platformEnableData.platform_enable[platformName] = {}; + } + + // 为所有插件设置相同的状态 + platformEnableData.plugins.forEach(plugin => { + // 如果onlyReserved为null,处理所有插件 + // 如果onlyReserved为true,只处理系统插件 + // 如果onlyReserved为false,只处理非系统插件 + if (onlyReserved === null || plugin.reserved === onlyReserved) { + platformEnableData.platform_enable[platformName][plugin.name] = isSelected; + } + }); +}; + +// 反选指定平台的所有插件 +const toggleAllPluginsForPlatform = (platformName) => { + // 确保平台存在于platform_enable中 + if (!platformEnableData.platform_enable[platformName]) { + platformEnableData.platform_enable[platformName] = {}; + } + + // 对每个插件进行反选操作 + platformEnableData.plugins.forEach(plugin => { + const currentState = platformEnableData.platform_enable[platformName][plugin.name]; + platformEnableData.platform_enable[platformName][plugin.name] = !currentState; + }); +}; + // 生命周期 onMounted(async () => { await getExtensions(); @@ -261,6 +364,9 @@ onMounted(async () => { {{ showReserved ? '隐藏系统保留插件' : '显示系统保留插件' }} + + 平台命令配置 + \ No newline at end of file + + + From 01c02d5efa2f31946077b2fd72417f89b4c56953 Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Wed, 9 Apr 2025 18:11:35 +0800 Subject: [PATCH 12/31] =?UTF-8?q?perf:=20=E6=8F=90=E5=8F=96=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E6=B8=85=E7=90=86=E9=80=BB=E8=BE=91=E5=88=B0=20`=5Fpu?= =?UTF-8?q?rge=5Fmodules`=20=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/star/star_manager.py | 115 ++++++++++++------------------ 1 file changed, 47 insertions(+), 68 deletions(-) diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index c0a3aa38b..99ac14b0a 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -169,12 +169,16 @@ class PluginManager: def _get_plugin_related_modules( self, plugin_root_dir: str, is_reserved: bool = False ) -> list[str]: - """获取插件相关的所有模块名 + """获取与指定插件相关的所有已加载模块名 + + 根据插件根目录名和是否为保留插件,从 sys.modules 中筛选出相关的模块名 + Args: plugin_root_dir: 插件根目录名 - is_reserved: 是否是保留插件 + is_reserved: 是否是保留插件,影响模块路径前缀 + Returns: - 模块名列表 + list[str]: 与该插件相关的模块名列表 """ prefix = "packages." if is_reserved else "data.plugins." return [ @@ -183,6 +187,38 @@ class PluginManager: if key.startswith(f"{prefix}{plugin_root_dir}") ] + def _purge_modules( + self, + module_patterns: list[str] = None, + root_dir_name: str = None, + is_reserved: bool = False, + ): + """从 sys.modules 中移除指定的模块 + + 可以基于模块名模式或插件目录名移除模块,用于清理插件相关的模块缓存 + + Args: + module_patterns: 要移除的模块名模式列表(例如 ["data.plugins", "packages"]) + root_dir_name: 插件根目录名,用于移除与该插件相关的所有模块 + is_reserved: 插件是否为保留插件(影响模块路径前缀) + """ + if module_patterns: + for pattern in module_patterns: + for key in list(sys.modules.keys()): + if key.startswith(pattern): + del sys.modules[key] + logger.debug(f"删除模块 {key}") + + if root_dir_name: + for module_name in self._get_plugin_related_modules( + root_dir_name, is_reserved + ): + try: + del sys.modules[module_name] + logger.debug(f"删除模块 {module_name}") + except KeyError: + logger.warning(f"模块 {module_name} 未载入") + async def reload(self, specified_plugin_name=None): """重新加载插件 @@ -194,13 +230,6 @@ class PluginManager: tuple: 返回 load() 方法的结果,包含 (success, error_message) - success (bool): 重载是否成功 - error_message (str|None): 错误信息,成功时为 None - - 流程: - 1. 如果指定了插件名,查找对应的模块路径 - 2. 终止现有插件实例 - 3. 解绑插件相关的处理器和注册信息 - 4. 清理系统模块缓存 - 5. 重新加载插件 """ specified_module_path = None if specified_plugin_name: @@ -226,9 +255,6 @@ class PluginManager: star_handlers_registry.clear() star_map.clear() star_registry.clear() - for key in list(sys.modules.keys()): - if key.startswith("data.plugins") or key.startswith("packages"): - del sys.modules[key] else: # 只重载指定插件 smd = star_map.get(specified_module_path) @@ -243,14 +269,6 @@ class PluginManager: await self._unbind_plugin(smd.name, specified_module_path) - for module_name in self._get_plugin_related_modules( - smd.root_dir_name, smd.reserved - ): - try: - del sys.modules[module_name] - except KeyError: - pass - return await self.load(specified_module_path) async def load(self, specified_module_path=None, specified_dir_name=None): @@ -265,23 +283,6 @@ class PluginManager: tuple: (success, error_message) - success (bool): 是否全部加载成功 - error_message (str|None): 错误信息,成功时为 None - - 执行步骤: - 1. 获取已禁用的插件和 LLM 工具列表 - 2. 获取所有插件模块信息 - 3. 对每个插件模块: - - 导入模块并实例化插件类 - - 加载插件配置(如果存在 _conf_schema.json) - - 处理插件元数据(metadata) - - 绑定事件处理器和 LLM 工具 - - 执行插件的 initialize() 方法 - 4. 记录加载失败的插件信息 - 5. 清理 pip.main 产生的日志处理器 - - 注意: - - 插件可以通过装饰器方式注册(v3.4.0+)或旧版本方式注册 - - 禁用的插件不会被实例化 - - 支持自定义命令权限过滤 """ inactivated_plugins: list = sp.get("inactivated_plugins", []) inactivated_llm_tools: list = sp.get("inactivated_llm_tools", []) @@ -515,24 +516,19 @@ class PluginManager: return False, fail_rec async def install_plugin(self, repo_url: str, proxy=""): - """从仓库 URL 安装插件。 + """从仓库 URL 安装插件 - 参数: + 从指定的仓库 URL 下载并安装插件,然后加载该插件到系统中 + + Args: repo_url (str): 要安装的插件仓库 URL proxy (str, optional): 用于下载的代理服务器。默认为空字符串。 - 返回: + Returns: dict | None: 安装成功时返回包含插件信息的字典: - repo: 插件的仓库 URL - readme: README.md 文件的内容(如果存在) 如果找不到插件元数据则返回 None。 - - 函数执行步骤: - 1. 从仓库下载并安装插件 - 2. 将插件重新加载到系统中 - 3. 尝试使用目录名称查找插件元数据 - 4. 提取 README.md 内容(如果可用) - 5. 返回仓库和说明文档信息 """ plugin_path = await self.updator.install(repo_url, proxy) # reload the plugin @@ -575,12 +571,6 @@ class PluginManager: Raises: Exception: 当插件不存在、是保留插件时,或删除插件文件夹失败时抛出异常 - - 执行以下步骤: - 1. 检查插件是否存在且不是保留插件 - 2. 调用插件的终止方法 - 3. 从注册表中解绑插件 - 4. 删除插件文件夹 """ plugin = self.context.get_registered_star(plugin_name) if not plugin: @@ -615,12 +605,6 @@ class PluginManager: Args: plugin_name: 要解绑的插件名称 plugin_module_path: 插件的完整模块路径 - - 该方法会执行以下操作: - 1. 从 star_map 和 star_registry 中移除插件 - 2. 移除该插件注册的所有处理函数 - 3. 清理处理函数映射 - 4. 从 sys.modules 中卸载所有相关的插件模块 """ plugin = None del star_map[plugin_module_path] @@ -644,14 +628,9 @@ class PluginManager: ]: del star_handlers_registry.star_handlers_map[k] - for module_name in self._get_plugin_related_modules( - plugin.root_dir_name, plugin.reserved - ): - try: - del sys.modules[module_name] - logger.debug(f"删除模块 {module_name}") - except KeyError: - logger.warning(f"模块 {module_name} 未载入") + self._purge_modules( + root_dir_name=plugin.root_dir_name, is_reserved=plugin.reserved + ) async def update_plugin(self, plugin_name: str, proxy=""): """升级一个插件""" From e349671fdf16fabbbef7672ef007ffc28aa51dea Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Wed, 9 Apr 2025 18:45:40 +0800 Subject: [PATCH 13/31] format --- astrbot/core/pipeline/process_stage/method/llm_request.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 3a129ea80..a9f793626 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -352,8 +352,6 @@ class LLMRequestSubStage(Stage): llm_response.tools_call_args, llm_response.tools_call_ids, ): - - try: func_tool = req.func_tool.get_func(func_tool_name) if func_tool.origin == "mcp": From 0b381e25708b55e5afc7aee55a48665d9d9b328d Mon Sep 17 00:00:00 2001 From: Raila23 <3271405327@qq.com> Date: Wed, 9 Apr 2025 22:10:56 +0800 Subject: [PATCH 14/31] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=A1=B9=EF=BC=8C=E5=85=81=E8=AE=B8=E9=85=8D=E7=BD=AE=EF=BC=9A?= =?UTF-8?q?=E8=B6=85=E5=87=BA=E6=9C=80=E5=A4=9A=E6=90=BA=E5=B8=A6=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=95=B0=E9=87=8F=20=E6=97=B6=EF=BC=8C=E4=B8=80?= =?UTF-8?q?=E6=AC=A1=E6=80=A7=E4=B8=A2=E5=BC=83=E5=A4=9A=E5=B0=91=E6=9D=A1?= =?UTF-8?q?=E6=97=A7=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/pipeline/process_stage/method/llm_request.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index c6a87b37c..35e9f5f50 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -38,6 +38,9 @@ class LLMRequestSubStage(Stage): self.max_context_length = ctx.astrbot_config["provider_settings"][ "max_context_length" ] # int + self.dequeue_context_length = ctx.astrbot_config["provider_settings"][ + "dequeue_context_length" + ] # int self.streaming_response = ctx.astrbot_config["provider_settings"][ "streaming_response" ] # bool @@ -135,7 +138,7 @@ class LLMRequestSubStage(Stage): and len(req.contexts) // 2 > self.max_context_length ): logger.debug("上下文长度超过限制,将截断。") - req.contexts = req.contexts[-self.max_context_length * 2 :] + req.contexts = req.contexts[-(self.max_context_length - self.dequeue_context_length) * 2 :] # session_id if not req.session_id: From 3d22772d4efbdeca4475ff0a4a9e19bdb3ed6853 Mon Sep 17 00:00:00 2001 From: Raila23 <3271405327@qq.com> Date: Wed, 9 Apr 2025 22:12:02 +0800 Subject: [PATCH 15/31] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=A1=B9=EF=BC=8C=E5=85=81=E8=AE=B8=E9=85=8D=E7=BD=AE=EF=BC=9A?= =?UTF-8?q?=E8=B6=85=E5=87=BA=E6=9C=80=E5=A4=9A=E6=90=BA=E5=B8=A6=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=95=B0=E9=87=8F=20=E6=97=B6=EF=BC=8C=E4=B8=80?= =?UTF-8?q?=E6=AC=A1=E6=80=A7=E4=B8=A2=E5=BC=83=E5=A4=9A=E5=B0=91=E6=9D=A1?= =?UTF-8?q?=E6=97=A7=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 13be0b498..fa1f70f34 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -50,6 +50,7 @@ DEFAULT_CONFIG = { "default_personality": "default", "prompt_prefix": "", "max_context_length": -1, + "dequeue_context_length": 1, "streaming_response": False, }, "provider_stt_settings": { @@ -994,6 +995,11 @@ CONFIG_METADATA_2 = { "type": "int", "hint": "超出这个数量时将丢弃最旧的部分,用户和AI的一轮聊天记为 1 条。-1 表示不限制,默认为不限制。", }, + "dequeue_context_length": { + "description": "丢弃对话数量(条)", + "type": "int", + "hint": "超出 最多携带对话数量(条) 时,丢弃多少条记录,用户和AI的一轮聊天记为 1 条。", + }, "streaming_response": { "description": "启用流式回复", "type": "bool", From 4d414a2994d41dc213250eee451bb4d469a22068 Mon Sep 17 00:00:00 2001 From: Raila23 <3271405327@qq.com> Date: Wed, 9 Apr 2025 22:28:33 +0800 Subject: [PATCH 16/31] =?UTF-8?q?=E5=A2=9E=E5=8A=A0dequeue=5Fcontext=5Flen?= =?UTF-8?q?gth=E7=9A=84=E5=80=BC=E7=9A=84=E5=88=A4=E6=96=AD=EF=BC=8C?= =?UTF-8?q?=E5=8F=AA=E8=83=BD=E5=9C=A81=E5=88=B0max=5Fcontext=5Flength?= =?UTF-8?q?=E4=B9=8B=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/pipeline/process_stage/method/llm_request.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 35e9f5f50..1ce44cee8 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -38,9 +38,10 @@ class LLMRequestSubStage(Stage): self.max_context_length = ctx.astrbot_config["provider_settings"][ "max_context_length" ] # int - self.dequeue_context_length = ctx.astrbot_config["provider_settings"][ - "dequeue_context_length" - ] # int + self.dequeue_context_length = min( + max(1,ctx.astrbot_config["provider_settings"]["dequeue_context_length"]), + self.max_context_length - 1 + ) # int self.streaming_response = ctx.astrbot_config["provider_settings"][ "streaming_response" ] # bool From 0e70f76c86033862ce40477e4d50760c600116ce Mon Sep 17 00:00:00 2001 From: baiiylu <62942942+zsbai@users.noreply.github.com> Date: Thu, 10 Apr 2025 08:03:38 +0800 Subject: [PATCH 17/31] fix: wrong type of sender_id returned in `event.get_sender_id()` --- .../platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index e41071a56..4a328a8c1 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -109,7 +109,7 @@ class AiocqhttpAdapter(Platform): """OneBot V11 请求类事件""" abm = AstrBotMessage() abm.self_id = str(event.self_id) - abm.sender = MessageMember(user_id=event.user_id, nickname=event.user_id) + abm.sender = MessageMember(user_id=str(event.user_id), nickname=event.user_id) abm.type = MessageType.OTHER_MESSAGE if "group_id" in event and event["group_id"]: abm.type = MessageType.GROUP_MESSAGE @@ -129,7 +129,7 @@ class AiocqhttpAdapter(Platform): """OneBot V11 通知类事件""" abm = AstrBotMessage() abm.self_id = str(event.self_id) - abm.sender = MessageMember(user_id=event.user_id, nickname=event.user_id) + abm.sender = MessageMember(user_id=str(event.user_id), nickname=event.user_id) abm.type = MessageType.OTHER_MESSAGE if "group_id" in event and event["group_id"]: abm.group_id = str(event.group_id) From 95b08b20235c7fcfc27745dedb6de6ac57cc883f Mon Sep 17 00:00:00 2001 From: Raila23 <3271405327@qq.com> Date: Thu, 10 Apr 2025 09:18:58 +0800 Subject: [PATCH 18/31] =?UTF-8?q?fix:=E4=BD=BF=20begin=5Fdialogs=20,?= =?UTF-8?q?=E9=A2=84=E8=AE=BE=E5=AF=B9=E8=AF=9D=EF=BC=8C=E4=B8=8D=E4=BC=9A?= =?UTF-8?q?=E5=A4=9A=E6=AC=A1=E6=8F=92=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/astrbot/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astrbot/main.py b/packages/astrbot/main.py index 013dcb0f7..410151510 100644 --- a/packages/astrbot/main.py +++ b/packages/astrbot/main.py @@ -1232,7 +1232,7 @@ UID: {user_id} 此 ID 可用于设置管理员。 if mood_dialogs := persona["_mood_imitation_dialogs_processed"]: req.system_prompt += "\nHere are few shots of dialogs, you need to imitate the tone of 'B' in the following dialogs to respond:\n" req.system_prompt += mood_dialogs - if begin_dialogs := persona["_begin_dialogs_processed"]: + if (begin_dialogs := persona["_begin_dialogs_processed"]) and not req.contexts: req.contexts[:0] = begin_dialogs if quote and quote.message_str: From cd18806c393a0c6b40d3600688fca4c55401eab1 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 10 Apr 2025 11:01:04 +0800 Subject: [PATCH 19/31] perf: improve platform compatibility checks --- astrbot/core/config/default.py | 3 +++ astrbot/core/pipeline/platform_compatibility/stage.py | 4 ++++ astrbot/core/utils/shared_preferences.py | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 13be0b498..4a6364979 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -248,6 +248,9 @@ CONFIG_METADATA_2 = { "description": "平台设置", "type": "object", "items": { + "plugin_enable": { + "invisible": True, # 隐藏插件启用配置 + }, "unique_session": { "description": "会话隔离", "type": "bool", diff --git a/astrbot/core/pipeline/platform_compatibility/stage.py b/astrbot/core/pipeline/platform_compatibility/stage.py index 0b0b04fda..644912c26 100644 --- a/astrbot/core/pipeline/platform_compatibility/stage.py +++ b/astrbot/core/pipeline/platform_compatibility/stage.py @@ -3,6 +3,7 @@ from ..context import PipelineContext from typing import Union, AsyncGenerator from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.star.star import star_map +from astrbot.core.star.star_handler import StarHandlerMetadata from astrbot.core import logger @@ -34,6 +35,8 @@ class PlatformCompatibilityStage(Stage): # 标记不兼容的处理器 for handler in activated_handlers: + if not isinstance(handler, StarHandlerMetadata): + continue # 检查处理器是否在当前平台启用 enabled = handler.is_enabled_for_platform(platform_id) if not enabled: @@ -43,6 +46,7 @@ class PlatformCompatibilityStage(Stage): f"[PlatformCompatibilityStage] 插件 {plugin_name} 在平台 {platform_id} 未启用,标记处理器 {handler.handler_name} 为平台不兼容" ) # 设置处理器为平台不兼容状态 + # TODO: 更好的标记方式 handler.platform_compatible = False else: # 确保处理器为平台兼容状态 diff --git a/astrbot/core/utils/shared_preferences.py b/astrbot/core/utils/shared_preferences.py index bf88ba8db..b11987322 100644 --- a/astrbot/core/utils/shared_preferences.py +++ b/astrbot/core/utils/shared_preferences.py @@ -15,7 +15,7 @@ class SharedPreferences: def _save_preferences(self): with open(self.path, "w") as f: - json.dump(self._data, f, indent=4) + json.dump(self._data, f, indent=4, ensure_ascii=False) f.flush() def get(self, key, default=None): From 87c3aff4ce3f37e67ffc21ad49c315b2c303dc0d Mon Sep 17 00:00:00 2001 From: anka <1350989414@qq.com> Date: Thu, 10 Apr 2025 11:25:03 +0800 Subject: [PATCH 20/31] =?UTF-8?q?perf:=20=E7=AE=80=E5=8C=96llm=5Frequest?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E6=B6=88=E6=81=AF=E6=88=90?= =?UTF-8?q?=E5=AF=B9=E9=AA=8C=E8=AF=81=E9=80=BB=E8=BE=91,=20=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E4=B8=A4=E5=A4=84=E9=AA=8C=E8=AF=81=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=88=B0=E4=B8=80=E4=B8=AA=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../process_stage/method/llm_request.py | 118 ++++++++++-------- 1 file changed, 65 insertions(+), 53 deletions(-) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 8a7062fd7..756f10b66 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -67,48 +67,10 @@ class LLMRequestSubStage(Stage): ), "provider_request 必须是 ProviderRequest 类型。" if req.conversation: - all_contexts = json.loads(req.conversation.history) - req.contexts = [] - i = 0 - while i < len(all_contexts): - current_msg = all_contexts[i] - # 普通消息 - if "_tool_call_history" not in current_msg: - req.contexts.append(current_msg) - i += 1 - continue - - # 工具调用消息, 必须成对出现 - if ( - current_msg.get("role") == "assistant" - and "tool_calls" in current_msg - ): - # 寻找tool响应 - assistant_msg = current_msg.copy() - # 移除标记 - if "_tool_call_history" in assistant_msg: - del assistant_msg["_tool_call_history"] - - related_tools = [] - j = i + 1 - while ( - j < len(all_contexts) - and all_contexts[j].get("role") == "tool" - and "_tool_call_history" in all_contexts[j] - ): - tool_msg = all_contexts[j].copy() - del tool_msg["_tool_call_history"] - related_tools.append(tool_msg) - j += 1 - - # 只添加成对的tool_call和tool响应 - if related_tools: - req.contexts.append(assistant_msg) - req.contexts.extend(related_tools) - # 已处理的消息跳过 - i = j - else: - i += 1 + all_contexts = json.load(req.conversation.history) + req.contexts = self._process_tool_message_pairs( + all_contexts, remove_tags=True + ) else: req = ProviderRequest(prompt="", image_urls=[]) @@ -485,21 +447,15 @@ class LLMRequestSubStage(Stage): if req.tool_calls_result: tool_calls_messages = req.tool_calls_result.to_openai_messages() - # 对顺序的验证 - assistant_msgs = [] - tool_msgs = [] - + # 添加标记 for message in tool_calls_messages: message["_tool_call_history"] = True - if message.get("role") == "assistant": - assistant_msgs.append(message) - elif message.get("role") == "tool": - tool_msgs.append(message) + processed_tool_messages = self._process_tool_message_pairs( + tool_calls_messages, remove_tags=False + ) - # 先添加assistant再添加tool - contexts.extend(assistant_msgs) - contexts.extend(tool_msgs) + contexts.extend(processed_tool_messages) contexts.append( {"role": "assistant", "content": llm_response.completion_text} @@ -510,3 +466,59 @@ class LLMRequestSubStage(Stage): await self.conv_manager.update_conversation( event.unified_msg_origin, req.conversation.cid, history=contexts_to_save ) + + def _process_tool_message_pairs(self, messages, remove_tags=True): + """处理工具调用消息,确保assistant和tool消息成对出现 + + Args: + messages (list): 消息列表 + remove_tags (bool): 是否移除_tool_call_history标记 + + Returns: + list: 处理后的消息列表,保证了assistant和对应tool消息的成对出现 + """ + result = [] + i = 0 + + while i < len(messages): + current_msg = messages[i] + + # 普通消息直接添加 + if "_tool_call_history" not in current_msg: + result.append(current_msg.copy() if remove_tags else current_msg) + i += 1 + continue + + # 工具调用消息成对处理 + if current_msg.get("role") == "assistant" and "tool_calls" in current_msg: + assistant_msg = current_msg.copy() + + if remove_tags and "_tool_call_history" in assistant_msg: + del assistant_msg["_tool_call_history"] + + related_tools = [] + j = i + 1 + while ( + j < len(messages) + and messages[j].get("role") == "tool" + and "_tool_call_history" in messages[j] + ): + tool_msg = messages[j].copy() + + if remove_tags: + del tool_msg["_tool_call_history"] + + related_tools.append(tool_msg) + j += 1 + + # 成对的时候添加到结果 + if related_tools: + result.append(assistant_msg) + result.extend(related_tools) + + i = j # 跳过已处理 + else: + # 单独的tool消息 + i += 1 + + return result From bdf25976a34508afb39363e9d4d664fd5c08d4b5 Mon Sep 17 00:00:00 2001 From: anka <1350989414@qq.com> Date: Thu, 10 Apr 2025 11:28:47 +0800 Subject: [PATCH 21/31] =?UTF-8?q?fix:=20=E5=B0=91=E6=89=93=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/pipeline/process_stage/method/llm_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 756f10b66..b7d279996 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -67,7 +67,7 @@ class LLMRequestSubStage(Stage): ), "provider_request 必须是 ProviderRequest 类型。" if req.conversation: - all_contexts = json.load(req.conversation.history) + all_contexts = json.loads(req.conversation.history) req.contexts = self._process_tool_message_pairs( all_contexts, remove_tags=True ) From a1481fb17970e8b8d9b323754ed011864eb188d2 Mon Sep 17 00:00:00 2001 From: Raven95676 Date: Thu, 10 Apr 2025 14:54:25 +0800 Subject: [PATCH 22/31] =?UTF-8?q?=E7=BE=A4=E8=81=8A=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E7=89=B9=E6=AE=8A=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/platform/sources/telegram/tg_adapter.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py index 332112880..4aefdb973 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -247,6 +247,16 @@ class TelegramPlatformAdapter(Platform): # 处理文本消息 plain_text = update.message.text + # 群聊场景命令特殊处理 + if plain_text.startswith("/"): + command_parts = plain_text.split(" ", 1) + if "@" in command_parts[0]: + command, bot_name = command_parts[0].split("@") + if bot_name == self.client.username: + plain_text = command + ( + f" {command_parts[1]}" if len(command_parts) > 1 else "" + ) + if update.message.entities: for entity in update.message.entities: if entity.type == "mention": From c0fadb45ab48ef465945d5e646d2d144a062e861 Mon Sep 17 00:00:00 2001 From: Raila23 <3271405327@qq.com> Date: Thu, 10 Apr 2025 15:20:56 +0800 Subject: [PATCH 23/31] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9B=B4=E8=AF=A6?= =?UTF-8?q?=E7=BB=86=E7=9A=84=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index fa1f70f34..32f77e7a5 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -998,7 +998,7 @@ CONFIG_METADATA_2 = { "dequeue_context_length": { "description": "丢弃对话数量(条)", "type": "int", - "hint": "超出 最多携带对话数量(条) 时,丢弃多少条记录,用户和AI的一轮聊天记为 1 条。", + "hint": "超出 最多携带对话数量(条) 时,丢弃多少条记录,用户和AI的一轮聊天记为 1 条。适宜的配置,可以提高超长上下文对话 deepseek 命中缓存效果,理想情况下计费将降低到1/3以下", }, "streaming_response": { "description": "启用流式回复", From 43d57f6dcb4f879590575259614a247e5c536760 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 10 Apr 2025 15:56:14 +0800 Subject: [PATCH 24/31] =?UTF-8?q?=F0=9F=8E=88=20perf:=20Add=20type=20valid?= =?UTF-8?q?ation=20for=20configuration=20items=20in=20validate=5Fconfig=20?= =?UTF-8?q?function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/dashboard/routes/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index 2747865e4..b2677de10 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -60,11 +60,13 @@ def validate_config( data[key] = False continue meta = metadata[key] + if "type" not in meta: + logger.debug(f"配置项 {path}{key} 没有类型定义, 跳过校验") + continue # null 转换 if value is None: data[key] = DEFAULT_VALUE_MAP[meta["type"]] continue - # 递归验证 if meta["type"] == "list" and not isinstance(value, list): errors.append( f"错误的类型 {path}{key}: 期望是 list, 得到了 {type(value).__name__}" From 967198fae0a90d3177cf1103b634a9305f04e031 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 10 Apr 2025 17:12:26 +0800 Subject: [PATCH 25/31] =?UTF-8?q?=E2=9C=A8=20feat:=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E9=A3=9E=E4=B9=A6=E5=B9=B3=E5=8F=B0=E4=B8=8B=E4=B8=BB=E5=8A=A8?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=8F=91=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes: #1177 WARNING: 这个修复会导致开启对话隔离下飞书群组的对话记录丢失(但没有被删除)。 --- .../platform/sources/lark/lark_adapter.py | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/astrbot/core/platform/sources/lark/lark_adapter.py b/astrbot/core/platform/sources/lark/lark_adapter.py index 8ea2ce36b..4a7ca0966 100644 --- a/astrbot/core/platform/sources/lark/lark_adapter.py +++ b/astrbot/core/platform/sources/lark/lark_adapter.py @@ -2,6 +2,7 @@ import base64 import asyncio import json import re +import uuid import astrbot.api.message_components as Comp from astrbot.api.platform import ( @@ -66,7 +67,41 @@ class LarkPlatformAdapter(Platform): async def send_by_session( self, session: MessageSesion, message_chain: MessageChain ): - raise NotImplementedError("Lark 适配器不支持 send_by_session") + res = await LarkMessageEvent._convert_to_lark(message_chain, self.lark_api) + wrapped = { + "zh_cn": { + "title": "", + "content": res, + } + } + + if session.message_type == MessageType.GROUP_MESSAGE: + id_type = "chat_id" + if "%" in session.session_id: + session.session_id = session.session_id.split("%")[1] + else: + id_type = "open_id" + + request = ( + CreateMessageRequest.builder() + .receive_id_type(id_type) + .request_body( + CreateMessageRequestBody.builder() + .receive_id(session.session_id) + .content(json.dumps(wrapped)) + .msg_type("post") + .uuid(str(uuid.uuid4())) + .build() + ) + .build() + ) + + response = await self.lark_api.im.v1.message.acreate(request) + + if not response.success(): + logger.error(f"发送飞书消息失败({response.code}): {response.msg}") + + await super().send_by_session(session, message_chain) def meta(self) -> PlatformMetadata: return PlatformMetadata( @@ -166,7 +201,10 @@ class LarkPlatformAdapter(Platform): else: abm.session_id = abm.sender.user_id else: - abm.session_id = abm.sender.user_id + if abm.type == MessageType.GROUP_MESSAGE: + abm.session_id = f"{abm.sender.user_id}%{abm.group_id}" # 也保留群组id + else: + abm.session_id = abm.sender.user_id logger.debug(abm) await self.handle_msg(abm) From 09482799c9d4e1142c5e36e127ee9d83289abc19 Mon Sep 17 00:00:00 2001 From: kuangfeng <1507173209@qq.com> Date: Thu, 10 Apr 2025 21:43:12 +0800 Subject: [PATCH 26/31] =?UTF-8?q?feat:=E4=B8=BA=E9=9C=80=E8=A6=81msg=5Fseq?= =?UTF-8?q?=E7=9A=84playload=E6=B7=BB=E5=8A=A0=E9=9A=8F=E6=9C=BAmsg=5Fseq?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/sources/qqofficial/qqofficial_message_event.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py index f74edd1ce..55f1e8c44 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -11,6 +11,7 @@ from botpy import Client from botpy.http import Route from astrbot.api import logger from botpy.types import message +import random class QQOfficialMessageEvent(AstrMessageEvent): @@ -68,7 +69,6 @@ class QQOfficialMessageEvent(AstrMessageEvent): return await super().send_streaming(generator) async def _post_send(self, stream: dict = None): - """QQ 官方 API 仅支持回复一次""" if not self.send_buffer: return @@ -97,6 +97,9 @@ class QQOfficialMessageEvent(AstrMessageEvent): "msg_id": self.message_obj.message_id, } + if not isinstance(source, (botpy.message.Message,botpy.message.DirectMessage)): + payload["msg_seq"] = random.randint(1, 10000) + match type(source): case botpy.message.GroupMessage: if image_base64: From caafb73190eef68363b31b7d700e94d8bca052dd Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Thu, 10 Apr 2025 23:28:51 +0800 Subject: [PATCH 27/31] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E8=B0=83=E7=94=A8=E7=9A=84=E4=B8=80=E4=BA=9B?= =?UTF-8?q?bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../process_stage/method/llm_request.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/astrbot/core/pipeline/process_stage/method/llm_request.py b/astrbot/core/pipeline/process_stage/method/llm_request.py index 547099e21..a7ddd9607 100644 --- a/astrbot/core/pipeline/process_stage/method/llm_request.py +++ b/astrbot/core/pipeline/process_stage/method/llm_request.py @@ -39,8 +39,8 @@ class LLMRequestSubStage(Stage): "max_context_length" ] # int self.dequeue_context_length = min( - max(1,ctx.astrbot_config["provider_settings"]["dequeue_context_length"]), - self.max_context_length - 1 + max(1, ctx.astrbot_config["provider_settings"]["dequeue_context_length"]), + self.max_context_length - 1, ) # int self.streaming_response = ctx.astrbot_config["provider_settings"][ "streaming_response" @@ -141,7 +141,9 @@ class LLMRequestSubStage(Stage): and len(req.contexts) // 2 > self.max_context_length ): logger.debug("上下文长度超过限制,将截断。") - req.contexts = req.contexts[-(self.max_context_length - self.dequeue_context_length) * 2 :] + req.contexts = req.contexts[ + -(self.max_context_length - self.dequeue_context_length) * 2 : + ] # session_id if not req.session_id: @@ -376,9 +378,14 @@ class LLMRequestSubStage(Stage): else: # 获取处理器,过滤掉平台不兼容的处理器 platform_id = event.get_platform_id() - if not func_tool.handler.is_enabled_for_platform(platform_id): + star_md = star_map.get(func_tool.handler_module_path) + if ( + star_md and + platform_id in star_md.supported_platforms + and not star_md.supported_platforms[platform_id] + ): logger.debug( - f"处理器 {func_tool_name} 在当前平台不兼容,跳过执行" + f"处理器 {func_tool_name}({star_md.name}) 在当前平台不兼容或者被禁用,跳过执行" ) # 直接跳过,不添加任何消息到tool_call_result continue From a4f212a18f3dd00496aabd398bcd31dbe5b7d72f Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 11 Apr 2025 00:20:08 +0800 Subject: [PATCH 28/31] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20OneAPI=20+=20Gemini(openai)=20=E4=BC=A0?= =?UTF-8?q?=E9=80=92=E7=A9=BA=E5=8F=82=E6=95=B0=E5=87=BD=E6=95=B0=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=97=B6=E5=8F=AF=E8=83=BD=E6=8A=A5=E9=94=99=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes: #1060 --- astrbot/core/provider/func_tool_manager.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/astrbot/core/provider/func_tool_manager.py b/astrbot/core/provider/func_tool_manager.py index 55c53eb3a..354594d88 100644 --- a/astrbot/core/provider/func_tool_manager.py +++ b/astrbot/core/provider/func_tool_manager.py @@ -348,16 +348,17 @@ class FuncCall: for f in self.func_list: if not f.active: continue - _l.append( - { - "type": "function", - "function": { - "name": f.name, - "parameters": f.parameters, - "description": f.description, - }, - } - ) + func_ = { + "type": "function", + "function": { + "name": f.name, + # "parameters": f.parameters, + "description": f.description, + }, + } + if f.parameters.get("properties"): + func_["function"]["parameters"] = f.parameters + _l.append(func_) return _l def get_func_desc_anthropic_style(self) -> list: From 8e949370600a192221ff26414e09934a501c71de Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 11 Apr 2025 15:50:36 +0800 Subject: [PATCH 29/31] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20gemini=20=E6=97=B6=EF=BC=8C=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E6=95=B0=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E4=BC=9A?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E8=B0=83=E7=94=A8=E5=B7=B2=E7=BB=8F=E5=9C=A8?= =?UTF-8?q?=E8=BF=87=E5=8E=BB=E4=BC=9A=E8=AF=9D=E4=B8=AD=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E8=BF=87=E7=9A=84=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes: #863 #1150 --- astrbot/core/provider/entities.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/astrbot/core/provider/entities.py b/astrbot/core/provider/entities.py index 99824fd0e..d0a9d30a5 100644 --- a/astrbot/core/provider/entities.py +++ b/astrbot/core/provider/entities.py @@ -212,9 +212,9 @@ class LLMResponse: role: str, completion_text: str = "", result_chain: MessageChain = None, - tools_call_args: List[Dict[str, any]] = [], - tools_call_name: List[str] = [], - tools_call_ids: List[str] = [], + tools_call_args: List[Dict[str, any]] = None, + tools_call_name: List[str] = None, + tools_call_ids: List[str] = None, raw_completion: ChatCompletion = None, _new_record: Dict[str, any] = None, is_chunk: bool = False, @@ -229,6 +229,13 @@ class LLMResponse: tools_call_name (List[str], optional): 工具调用名称. Defaults to None. raw_completion (ChatCompletion, optional): 原始响应, OpenAI 格式. Defaults to None. """ + if tools_call_args is None: + tools_call_args = [] + if tools_call_name is None: + tools_call_name = [] + if tools_call_ids is None: + tools_call_ids = [] + self.role = role self.completion_text = completion_text self.result_chain = result_chain From 757d2a394797c1b5d560811e58bb8eaccf7b7fbb Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 11 Apr 2025 17:23:17 +0800 Subject: [PATCH 30/31] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=20Dify=20API=20=E7=B1=BB=E5=9E=8B=E6=8F=90=E7=A4=BA=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AF=B9=20Chatflow=20=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E7=9A=84=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 4 ++-- astrbot/core/provider/sources/dify_source.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 648b2bc9a..459ff622a 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -928,8 +928,8 @@ CONFIG_METADATA_2 = { "dify_api_type": { "description": "Dify 应用类型", "type": "string", - "hint": "Dify API 类型。根据 Dify 官网,目前支持 chat, agent, workflow 三种应用类型", - "options": ["chat", "agent", "workflow"], + "hint": "Dify API 类型。根据 Dify 官网,目前支持 chat, chatflow, agent, workflow 三种应用类型。", + "options": ["chat", "chatflow", "agent", "workflow"], }, "dify_workflow_output_key": { "description": "Dify Workflow 输出变量名", diff --git a/astrbot/core/provider/sources/dify_source.py b/astrbot/core/provider/sources/dify_source.py index 1adb0f884..78e3760c1 100644 --- a/astrbot/core/provider/sources/dify_source.py +++ b/astrbot/core/provider/sources/dify_source.py @@ -102,7 +102,7 @@ class ProviderDify(Provider): try: match self.api_type: - case "chat" | "agent": + case "chat" | "agent" | "chatflow": if not prompt: prompt = "请描述这张图片。" From 3b6dd7e15a8e096464adf439aff7022983503b91 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Fri, 11 Apr 2025 17:27:29 +0800 Subject: [PATCH 31/31] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20dify=20=E4=B8=8B=E5=88=A0=E9=99=A4=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E7=9A=84=E6=8A=A5=E9=94=99=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes: #1226 --- packages/astrbot/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/astrbot/main.py b/packages/astrbot/main.py index 410151510..17ce50bd1 100644 --- a/packages/astrbot/main.py +++ b/packages/astrbot/main.py @@ -880,8 +880,9 @@ UID: {user_id} 此 ID 可用于设置管理员。 provider = self.context.get_using_provider() if provider and provider.meta().type == "dify": assert isinstance(provider, ProviderDify) - await provider.api_client.delete_chat_conv(message.unified_msg_origin) - provider.conversation_ids.pop(message.unified_msg_origin, None) + dify_cid = provider.conversation_ids.pop(message.unified_msg_origin, None) + if dify_cid: + await provider.api_client.delete_chat_conv(message.unified_msg_origin, dify_cid) message.set_result( MessageEventResult().message( "删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。"