feat:重构插件api
This commit is contained in:
206
src/plugins/README.md
Normal file
206
src/plugins/README.md
Normal 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. **测试覆盖**:为插件编写单元测试
|
||||
@@ -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 的聊天流
|
||||
```
|
||||
|
||||
根据错误信息进行相应的处理。
|
||||
30
src/plugins/examples/simple_plugin/config.toml
Normal file
30
src/plugins/examples/simple_plugin/config.toml
Normal 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]"
|
||||
195
src/plugins/examples/simple_plugin/plugin.py
Normal file
195
src/plugins/examples/simple_plugin/plugin.py
Normal 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)
|
||||
]
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user