feat:重构插件api

This commit is contained in:
SengokuCola
2025-06-10 15:28:36 +08:00
parent 2edece11ea
commit 4d32b3052f
30 changed files with 2429 additions and 471 deletions

206
src/plugins/README.md Normal file
View File

@@ -0,0 +1,206 @@
# MaiBot 插件系统架构
## 概述
MaiBot 插件系统采用组件化设计,支持插件包含多种组件类型:
- **Action组件**:处理聊天中的动作逻辑
- **Command组件**:处理命令请求
- **未来扩展**Scheduler定时任务、Listener事件监听
## 目录结构
```
src/plugins/
├── core/ # 插件核心管理
│ ├── plugin_manager.py # 插件管理器
│ ├── plugin_loader.py # 插件加载器(预留)
│ └── component_registry.py # 组件注册中心
├── apis/ # API模块
│ ├── plugin_api.py # 统一API聚合
│ ├── message_api.py # 消息API
│ ├── llm_api.py # LLM API
│ ├── database_api.py # 数据库API
│ ├── config_api.py # 配置API
│ ├── utils_api.py # 工具API
│ ├── stream_api.py # 流API
│ └── hearflow_api.py # 心流API
├── base/ # 基础类
│ ├── base_plugin.py # 插件基类
│ ├── base_action.py # Action组件基类
│ ├── base_command.py # Command组件基类
│ └── component_types.py # 组件类型定义
├── built_in/ # 内置组件
│ ├── actions/ # 内置Action
│ └── commands/ # 内置Command
└── examples/ # 示例插件
└── simple_plugin/ # 简单插件示例
├── plugin.py
└── config.toml
```
## 核心特性
### 1. 组件化设计
- 插件可以包含多种组件类型
- 每种组件有明确的职责和接口
- 支持组件的独立启用/禁用
### 2. 统一的API访问
- 所有插件组件通过 `PluginAPI` 访问系统功能
- 包含消息发送、数据库操作、LLM调用等
- 提供统一的错误处理和日志记录
### 3. 灵活的配置系统
- 支持 TOML 格式的配置文件
- 插件可以读取自定义配置
- 支持全局配置和插件特定配置
### 4. 统一的注册管理
- 组件注册中心管理所有组件
- 支持组件的动态启用/禁用
- 提供丰富的查询和统计接口
## 插件开发指南
### 创建基本插件
```python
from src.plugins.base.base_plugin import BasePlugin, register_plugin
from src.plugins.base.base_action import BaseAction
from src.plugins.base.component_types import ActionInfo, ActionActivationType
class MyAction(BaseAction):
async def execute(self) -> tuple[bool, str]:
# 使用API发送消息
response = "Hello from my plugin!"
return True, response
@register_plugin
class MyPlugin(BasePlugin):
plugin_name = "my_plugin"
plugin_description = "我的第一个插件"
def get_plugin_components(self):
action_info = ActionInfo(
name="my_action",
description="我的动作",
activation_keywords=["hello"]
)
return [(action_info, MyAction)]
```
### 创建命令组件
```python
from src.plugins.base.base_command import BaseCommand
from src.plugins.base.component_types import CommandInfo
class MyCommand(BaseCommand):
async def execute(self) -> tuple[bool, str]:
# 获取命令参数
param = self.matched_groups.get("param", "")
# 发送回复
await self.send_reply(f"收到参数: {param}")
return True, f"处理完成: {param}"
# 在插件中注册
def get_plugin_components(self):
command_info = CommandInfo(
name="my_command",
description="我的命令",
command_pattern=r"^/mycmd\s+(?P<param>\w+)$",
command_help="用法:/mycmd <参数>"
)
return [(command_info, MyCommand)]
```
### 使用配置文件
```toml
# config.toml
[plugin]
name = "my_plugin"
enabled = true
[my_settings]
max_items = 10
default_message = "Hello World"
```
```python
class MyPlugin(BasePlugin):
config_file_name = "config.toml"
def get_plugin_components(self):
# 读取配置
max_items = self.get_config("my_settings.max_items", 5)
message = self.get_config("my_settings.default_message", "Hi")
# 使用配置创建组件...
```
## API使用示例
### 消息操作
```python
# 发送文本消息
await self.api.send_text_to_group(chat_stream, "Hello!")
# 发送图片
await self.api.send_image_to_group(chat_stream, image_path)
```
### 数据库操作
```python
# 查询数据
data = await self.api.db_get("table_name", "key")
# 保存数据
await self.api.db_set("table_name", "key", "value")
```
### LLM调用
```python
# 生成文本
response = await self.api.llm_text_request("你好,请介绍一下自己")
# 生成图片
image_url = await self.api.llm_image_request("一只可爱的猫咪")
```
## 内置组件迁移
现有的内置Action和Command将迁移到新架构
### Action迁移
- `reply_action.py``src/plugins/built_in/actions/reply_action.py`
- `emoji_action.py``src/plugins/built_in/actions/emoji_action.py`
- `no_reply_action.py``src/plugins/built_in/actions/no_reply_action.py`
### Command迁移
- 现有命令系统将封装为内置Command组件
- 保持现有的命令模式和功能
## 兼容性
新插件系统保持与现有系统的兼容性:
- 现有的Action和Command继续工作
- 提供兼容层和适配器
- 逐步迁移到新架构
## 扩展性
系统设计支持未来扩展:
- 新的组件类型Scheduler、Listener等
- 插件间依赖和通信
- 插件热重载
- 插件市场和分发
## 最佳实践
1. **单一职责**:每个组件专注于特定功能
2. **配置驱动**:通过配置文件控制行为
3. **错误处理**:妥善处理异常情况
4. **日志记录**:记录关键操作和错误
5. **测试覆盖**:为插件编写单元测试

View File

@@ -1,105 +0,0 @@
# 发送消息命令插件
这个插件提供了多个便捷的消息发送命令,允许管理员向指定群聊或用户发送消息。
## 命令列表
### 1. `/send` - 基础发送命令
向指定群聊或用户发送文本消息。
**语法:**
```
/send <group|user> <ID> <消息内容>
```
**示例:**
```
/send group 123456789 大家好!
/send user 987654321 私聊消息
```
### 2. `/sendfull` - 增强发送命令
支持多种消息类型和平台的发送命令。
**语法:**
```
/sendfull <消息类型> <目标类型> <ID> [平台] <内容>
```
**消息类型:**
- `text` - 文本消息
- `image` - 图片消息提供图片URL
- `emoji` - 表情消息
**示例:**
```
/sendfull text group 123456789 qq 大家好!这是文本消息
/sendfull image user 987654321 https://example.com/image.jpg
/sendfull emoji group 123456789 😄
```
### 3. `/msg` - 快速群聊发送
快速向群聊发送文本消息的简化命令。
**语法:**
```
/msg <群ID> <消息内容>
```
**示例:**
```
/msg 123456789 大家好!
/msg 987654321 这是一条快速消息
```
### 4. `/pm` - 私聊发送
快速向用户发送私聊消息的命令。
**语法:**
```
/pm <用户ID> <消息内容>
```
**示例:**
```
/pm 123456789 你好!
/pm 987654321 这是私聊消息
```
## 使用前提
1. **目标存在**: 目标群聊或用户必须已经在机器人的数据库中存在对应的chat_stream记录
2. **权限要求**: 机器人必须在目标群聊中有发言权限
3. **管理员权限**: 这些命令通常需要管理员权限才能使用
## 错误处理
如果消息发送失败,可能的原因:
1. **目标不存在**: 指定的群ID或用户ID在数据库中找不到对应记录
2. **权限不足**: 机器人在目标群聊中没有发言权限
3. **网络问题**: 网络连接异常
4. **平台限制**: 目标平台的API限制
## 注意事项
1. **ID格式**: 群ID和用户ID必须是纯数字
2. **消息长度**: 注意平台对消息长度的限制
3. **图片格式**: 发送图片时需要提供有效的图片URL
4. **平台支持**: 目前主要支持QQ平台其他平台可能需要额外配置
## 安全建议
1. 限制这些命令的使用权限,避免滥用
2. 监控发送频率,防止刷屏
3. 定期检查发送日志,确保合规使用
## 故障排除
查看日志文件中的详细错误信息:
```
[INFO] [Command:send] 执行发送消息命令: group:123456789 -> 大家好!...
[ERROR] [Command:send] 发送群聊消息时出错: 未找到群ID为 123456789 的聊天流
```
根据错误信息进行相应的处理。

View File

@@ -0,0 +1,30 @@
# 完整示例插件配置文件
[plugin]
name = "simple_plugin"
version = "1.1.0"
enabled = true
description = "展示新插件系统完整功能的示例插件"
[hello_action]
greeting_message = "你好,{username}欢迎使用MaiBot新插件系统"
enable_emoji = true
enable_llm_greeting = false # 是否使用LLM生成个性化问候
default_username = "朋友"
[status_command]
show_detailed_info = true
allowed_types = ["系统", "插件", "数据库", "内存", "网络"]
default_type = "系统"
[echo_command]
max_message_length = 500
enable_formatting = true
[help_command]
show_extended_help = true
include_config_info = true
[logging]
level = "INFO"
prefix = "[SimplePlugin]"

View File

@@ -0,0 +1,195 @@
"""
完整示例插件
演示新插件系统的完整功能:
- 使用简化的导入接口
- 展示Action和Command组件的定义
- 展示插件配置的使用
- 提供实用的示例功能
- 演示API的多种使用方式
"""
from typing import List, Tuple, Type, Optional
# 使用简化的导入接口
from src.plugin_system import (
BasePlugin, register_plugin, BaseAction, BaseCommand,
ComponentInfo, ActionInfo, CommandInfo, ActionActivationType, ChatMode
)
from src.common.logger_manager import get_logger
logger = get_logger("simple_plugin")
class HelloAction(BaseAction):
"""智能问候Action组件"""
# ✅ 现在可以直接在类中定义激活条件!
focus_activation_type = ActionActivationType.KEYWORD
normal_activation_type = ActionActivationType.KEYWORD
activation_keywords = ["你好", "hello", "问候", "hi", ""]
keyword_case_sensitive = False
mode_enable = ChatMode.ALL
parallel_action = False
async def execute(self) -> Tuple[bool, str]:
"""执行问候动作"""
username = self.action_data.get("username", "朋友")
# 使用配置文件中的问候消息
plugin_instance = SimplePlugin()
greeting_template = plugin_instance.get_config("hello_action.greeting_message", "你好,{username}")
enable_emoji = plugin_instance.get_config("hello_action.enable_emoji", True)
enable_llm = plugin_instance.get_config("hello_action.enable_llm_greeting", False)
# 如果启用LLM生成个性化问候
if enable_llm:
try:
# 演示使用LLM API生成个性化问候
models = self.api.get_available_models()
if models:
first_model = list(models.values())[0]
prompt = f"为用户名叫{username}的朋友生成一句温暖的个性化问候语不超过30字"
success, response, _, _ = await self.api.generate_with_model(
prompt=prompt,
model_config=first_model
)
if success:
logger.info(f"{self.log_prefix} 使用LLM生成问候: {response}")
return True, response
except Exception as e:
logger.warning(f"{self.log_prefix} LLM生成问候失败使用默认模板: {e}")
# 构建基础问候消息
response = greeting_template.format(username=username)
if enable_emoji:
response += " 😊"
# 演示存储Action执行记录到数据库
await self.api.store_action_info(
action_build_into_prompt=False,
action_prompt_display=f"问候了用户: {username}",
action_done=True
)
logger.info(f"{self.log_prefix} 执行问候动作: {username}")
return True, response
class EchoCommand(BaseCommand):
"""回声命令 - 重复用户输入"""
# ✅ 现在可以直接在类中定义命令模式!
command_pattern = r"^/echo\s+(?P<message>.+)$"
command_help = "重复消息,用法:/echo <消息内容>"
command_examples = ["/echo Hello World", "/echo 你好世界"]
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行回声命令"""
# 获取匹配的参数
message = self.matched_groups.get("message", "")
if not message:
response = "请提供要重复的消息!用法:/echo <消息内容>"
else:
response = f"🔊 {message}"
# 发送回复
await self.send_reply(response)
logger.info(f"{self.log_prefix} 执行回声命令: {message}")
return True, response
class StatusCommand(BaseCommand):
"""状态查询Command组件"""
# ✅ 直接定义命令模式
command_pattern = r"^/status\s*(?P<type>\w+)?$"
command_help = "查询系统状态,用法:/status [类型]"
command_examples = ["/status", "/status 系统", "/status 插件"]
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行状态查询命令"""
# 获取匹配的参数
query_type = self.matched_groups.get("type", "系统")
# 从配置文件获取设置
plugin_instance = SimplePlugin()
show_detailed = plugin_instance.get_config("status_command.show_detailed_info", True)
allowed_types = plugin_instance.get_config("status_command.allowed_types", ["系统", "插件"])
if query_type not in allowed_types:
response = f"不支持的查询类型: {query_type}\n支持的类型: {', '.join(allowed_types)}"
elif show_detailed:
response = f"📊 {query_type}状态详情:\n✅ 运行正常\n🔧 版本: 1.0.0\n⚡ 性能: 良好"
else:
response = f"{query_type}状态:正常"
# 发送回复
await self.send_reply(response)
logger.info(f"{self.log_prefix} 执行状态查询: {query_type}")
return True, response
class HelpCommand(BaseCommand):
"""帮助命令 - 显示插件功能"""
# ✅ 直接定义命令模式
command_pattern = r"^/help$"
command_help = "显示插件帮助信息"
command_examples = ["/help"]
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行帮助命令"""
help_text = """
🤖 简单示例插件帮助
📝 可用命令:
• /echo <消息> - 重复你的消息
• /status [类型] - 查询系统状态
• /help - 显示此帮助信息
🎯 智能功能:
• 自动问候 - 当消息包含"你好""hello"等关键词时触发
⚙️ 配置:
本插件支持通过config.toml文件进行个性化配置
💡 这是新插件系统的完整示例展示了Action和Command的结合使用。
""".strip()
await self.send_reply(help_text)
logger.info(f"{self.log_prefix} 显示帮助信息")
return True, "已显示帮助信息"
@register_plugin
class SimplePlugin(BasePlugin):
"""完整示例插件
包含多个Action和Command组件展示插件系统的完整功能
"""
# 插件基本信息
plugin_name = "simple_plugin"
plugin_description = "完整的示例插件,展示新插件系统的各种功能"
plugin_version = "1.1.0"
plugin_author = "MaiBot开发团队"
enable_plugin = True
config_file_name = "config.toml" # 配置文件
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表"""
# ✅ 现在可以直接从类属性生成组件信息!
return [
(HelloAction.get_action_info("hello_action", "智能问候动作,支持自定义消息和表情"), HelloAction),
(EchoCommand.get_command_info("echo_command", "回声命令,重复用户输入的消息"), EchoCommand),
(StatusCommand.get_command_info("status_command", "状态查询命令,支持多种查询类型"), StatusCommand),
(HelpCommand.get_command_info("help_command", "帮助命令,显示插件功能说明"), HelpCommand)
]

View File

@@ -1,304 +0,0 @@
import importlib
import pkgutil
import os
from typing import Dict, Tuple
from src.common.logger_manager import get_logger
logger = get_logger("plugin_loader")
class PluginLoader:
"""统一的插件加载器负责加载插件的所有组件actions、commands等"""
def __init__(self):
self.loaded_actions = 0
self.loaded_commands = 0
self.plugin_stats: Dict[str, Dict[str, int]] = {} # 统计每个插件加载的组件数量
self.plugin_sources: Dict[str, str] = {} # 记录每个插件来自哪个路径
def load_all_plugins(self) -> Tuple[int, int]:
"""加载所有插件的所有组件
Returns:
Tuple[int, int]: (加载的动作数量, 加载的命令数量)
"""
# 定义插件搜索路径(优先级从高到低)
plugin_paths = [
("plugins", "plugins"), # 项目根目录的plugins文件夹
("src.plugins", os.path.join("src", "plugins")), # src下的plugins文件夹
]
total_plugins_found = 0
for plugin_import_path, plugin_dir_path in plugin_paths:
try:
plugins_loaded = self._load_plugins_from_path(plugin_import_path, plugin_dir_path)
total_plugins_found += plugins_loaded
except Exception as e:
logger.error(f"从路径 {plugin_dir_path} 加载插件失败: {e}")
import traceback
logger.error(traceback.format_exc())
if total_plugins_found == 0:
logger.info("未找到任何插件目录或插件")
# 输出加载统计
self._log_loading_stats()
return self.loaded_actions, self.loaded_commands
def _load_plugins_from_path(self, plugin_import_path: str, plugin_dir_path: str) -> int:
"""从指定路径加载插件
Args:
plugin_import_path: 插件的导入路径 (如 "plugins""src.plugins")
plugin_dir_path: 插件目录的文件系统路径
Returns:
int: 找到的插件包数量
"""
# 检查插件目录是否存在
if not os.path.exists(plugin_dir_path):
logger.debug(f"插件目录 {plugin_dir_path} 不存在,跳过")
return 0
logger.info(f"正在从 {plugin_dir_path} 加载插件...")
# 导入插件包
try:
plugins_package = importlib.import_module(plugin_import_path)
logger.info(f"成功导入插件包: {plugin_import_path}")
except ImportError as e:
logger.warning(f"导入插件包 {plugin_import_path} 失败: {e}")
return 0
# 遍历插件包中的所有子包
plugins_found = 0
for _, plugin_name, is_pkg in pkgutil.iter_modules(plugins_package.__path__, plugins_package.__name__ + "."):
if not is_pkg:
continue
logger.debug(f"检测到插件: {plugin_name}")
# 记录插件来源
self.plugin_sources[plugin_name] = plugin_dir_path
self._load_single_plugin(plugin_name)
plugins_found += 1
if plugins_found > 0:
logger.info(f"{plugin_dir_path} 找到 {plugins_found} 个插件包")
else:
logger.debug(f"{plugin_dir_path} 未找到任何插件包")
return plugins_found
def _load_single_plugin(self, plugin_name: str) -> None:
"""加载单个插件的所有组件
Args:
plugin_name: 插件名称
"""
plugin_stats = {"actions": 0, "commands": 0}
# 加载动作组件
actions_count = self._load_plugin_actions(plugin_name)
plugin_stats["actions"] = actions_count
self.loaded_actions += actions_count
# 加载命令组件
commands_count = self._load_plugin_commands(plugin_name)
plugin_stats["commands"] = commands_count
self.loaded_commands += commands_count
# 记录插件统计信息
if actions_count > 0 or commands_count > 0:
self.plugin_stats[plugin_name] = plugin_stats
logger.info(f"插件 {plugin_name} 加载完成: {actions_count} 个动作, {commands_count} 个命令")
def _load_plugin_actions(self, plugin_name: str) -> int:
"""加载插件的动作组件
Args:
plugin_name: 插件名称
Returns:
int: 加载的动作数量
"""
loaded_count = 0
# 优先检查插件是否有actions子包
plugin_actions_path = f"{plugin_name}.actions"
plugin_actions_dir = plugin_name.replace(".", os.path.sep) + os.path.sep + "actions"
actions_loaded_from_subdir = False
# 首先尝试从actions子目录加载
if os.path.exists(plugin_actions_dir):
loaded_count += self._load_from_actions_subdir(plugin_name, plugin_actions_path, plugin_actions_dir)
if loaded_count > 0:
actions_loaded_from_subdir = True
# 如果actions子目录不存在或加载失败尝试从插件根目录加载
if not actions_loaded_from_subdir:
loaded_count += self._load_actions_from_root_dir(plugin_name)
return loaded_count
def _load_plugin_commands(self, plugin_name: str) -> int:
"""加载插件的命令组件
Args:
plugin_name: 插件名称
Returns:
int: 加载的命令数量
"""
loaded_count = 0
# 优先检查插件是否有commands子包
plugin_commands_path = f"{plugin_name}.commands"
plugin_commands_dir = plugin_name.replace(".", os.path.sep) + os.path.sep + "commands"
commands_loaded_from_subdir = False
# 首先尝试从commands子目录加载
if os.path.exists(plugin_commands_dir):
loaded_count += self._load_from_commands_subdir(plugin_name, plugin_commands_path, plugin_commands_dir)
if loaded_count > 0:
commands_loaded_from_subdir = True
# 如果commands子目录不存在或加载失败尝试从插件根目录加载
if not commands_loaded_from_subdir:
loaded_count += self._load_commands_from_root_dir(plugin_name)
return loaded_count
def _load_from_actions_subdir(self, plugin_name: str, plugin_actions_path: str, plugin_actions_dir: str) -> int:
"""从actions子目录加载动作"""
loaded_count = 0
try:
# 尝试导入插件的actions包
actions_module = importlib.import_module(plugin_actions_path)
logger.debug(f"成功加载插件动作模块: {plugin_actions_path}")
# 遍历actions目录中的所有Python文件
actions_dir = os.path.dirname(actions_module.__file__)
for file in os.listdir(actions_dir):
if file.endswith(".py") and file != "__init__.py":
action_module_name = f"{plugin_actions_path}.{file[:-3]}"
try:
importlib.import_module(action_module_name)
logger.info(f"成功加载动作: {action_module_name}")
loaded_count += 1
except Exception as e:
logger.error(f"加载动作失败: {action_module_name}, 错误: {e}")
except ImportError as e:
logger.debug(f"插件 {plugin_name} 的actions子包导入失败: {e}")
return loaded_count
def _load_from_commands_subdir(self, plugin_name: str, plugin_commands_path: str, plugin_commands_dir: str) -> int:
"""从commands子目录加载命令"""
loaded_count = 0
try:
# 尝试导入插件的commands包
commands_module = importlib.import_module(plugin_commands_path)
logger.debug(f"成功加载插件命令模块: {plugin_commands_path}")
# 遍历commands目录中的所有Python文件
commands_dir = os.path.dirname(commands_module.__file__)
for file in os.listdir(commands_dir):
if file.endswith(".py") and file != "__init__.py":
command_module_name = f"{plugin_commands_path}.{file[:-3]}"
try:
importlib.import_module(command_module_name)
logger.info(f"成功加载命令: {command_module_name}")
loaded_count += 1
except Exception as e:
logger.error(f"加载命令失败: {command_module_name}, 错误: {e}")
except ImportError as e:
logger.debug(f"插件 {plugin_name} 的commands子包导入失败: {e}")
return loaded_count
def _load_actions_from_root_dir(self, plugin_name: str) -> int:
"""从插件根目录加载动作文件"""
loaded_count = 0
try:
# 导入插件包本身
plugin_module = importlib.import_module(plugin_name)
logger.debug(f"尝试从插件根目录加载动作: {plugin_name}")
# 遍历插件根目录中的所有Python文件
plugin_dir = os.path.dirname(plugin_module.__file__)
for file in os.listdir(plugin_dir):
if file.endswith(".py") and file != "__init__.py":
# 跳过非动作文件(根据命名约定)
if not (file.endswith("_action.py") or file.endswith("_actions.py") or "action" in file):
continue
action_module_name = f"{plugin_name}.{file[:-3]}"
try:
importlib.import_module(action_module_name)
logger.info(f"成功加载动作: {action_module_name}")
loaded_count += 1
except Exception as e:
logger.error(f"加载动作失败: {action_module_name}, 错误: {e}")
except ImportError as e:
logger.debug(f"插件 {plugin_name} 导入失败: {e}")
return loaded_count
def _load_commands_from_root_dir(self, plugin_name: str) -> int:
"""从插件根目录加载命令文件"""
loaded_count = 0
try:
# 导入插件包本身
plugin_module = importlib.import_module(plugin_name)
logger.debug(f"尝试从插件根目录加载命令: {plugin_name}")
# 遍历插件根目录中的所有Python文件
plugin_dir = os.path.dirname(plugin_module.__file__)
for file in os.listdir(plugin_dir):
if file.endswith(".py") and file != "__init__.py":
# 跳过非命令文件(根据命名约定)
if not (file.endswith("_command.py") or file.endswith("_commands.py") or "command" in file):
continue
command_module_name = f"{plugin_name}.{file[:-3]}"
try:
importlib.import_module(command_module_name)
logger.info(f"成功加载命令: {command_module_name}")
loaded_count += 1
except Exception as e:
logger.error(f"加载命令失败: {command_module_name}, 错误: {e}")
except ImportError as e:
logger.debug(f"插件 {plugin_name} 导入失败: {e}")
return loaded_count
def _log_loading_stats(self) -> None:
"""输出加载统计信息"""
logger.success(f"插件加载完成: 总计 {self.loaded_actions} 个动作, {self.loaded_commands} 个命令")
if self.plugin_stats:
logger.info("插件加载详情:")
for plugin_name, stats in self.plugin_stats.items():
plugin_display_name = plugin_name.split(".")[-1] # 只显示插件名称,不显示完整路径
source_path = self.plugin_sources.get(plugin_name, "未知路径")
logger.info(
f" {plugin_display_name} (来源: {source_path}): {stats['actions']} 动作, {stats['commands']} 命令"
)
# 创建全局插件加载器实例
plugin_loader = PluginLoader()