From c44811815ae3175d10c92d71dcaaf91e567b91ee Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Mon, 9 Jun 2025 23:51:12 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E7=BB=9F=E4=B8=80=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E6=8F=92=E4=BB=B6=EF=BC=8C=E5=8C=BA=E5=88=86=E5=86=85?= =?UTF-8?q?=E9=83=A8=E6=8F=92=E4=BB=B6=E5=92=8C=E5=A4=96=E9=83=A8=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E6=8F=90=E4=BE=9B=E7=A4=BA=E4=BE=8B=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E5=8F=91=E9=80=81=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 8 +- HEARFLOW_API_说明文档.md | 241 ++++++++++++++ docs/plugin_loading_paths.md | 119 +++++++ src/chat/actions/plugin_action.py | 3 +- src/chat/actions/plugin_api/__init__.py | 2 + src/chat/actions/plugin_api/hearflow_api.py | 134 ++++++++ src/chat/actions/plugin_api/message_api.py | 155 ++++++++- .../command_handler.py | 73 +---- src/main.py | 77 +---- src/plugins/doubao_pic/actions/pic_action.py | 2 +- src/plugins/example_command_plugin/README.md | 105 ++++++ .../__init__.py | 0 .../commands/__init__.py | 0 .../commands/custom_prefix_command.py | 0 .../commands/help_command.py | 0 .../commands/message_info_command.py | 282 ++++++++++++++++ .../commands/send_msg_commad.py | 119 +++++++ .../commands/send_msg_enhanced.py | 170 ++++++++++ .../commands/send_msg_with_context.py | 253 +++++++++++++++ .../example_commands/commands/echo_command.py | 36 --- src/plugins/plugin_loader.py | 303 ++++++++++++++++++ 消息发送API使用说明.md | 129 ++++++++ 22 files changed, 2029 insertions(+), 182 deletions(-) create mode 100644 HEARFLOW_API_说明文档.md create mode 100644 docs/plugin_loading_paths.md create mode 100644 src/chat/actions/plugin_api/hearflow_api.py rename src/chat/{message_receive => command}/command_handler.py (71%) create mode 100644 src/plugins/example_command_plugin/README.md rename src/plugins/{example_commands => example_command_plugin}/__init__.py (100%) rename src/plugins/{example_commands => example_command_plugin}/commands/__init__.py (100%) rename src/plugins/{example_commands => example_command_plugin}/commands/custom_prefix_command.py (100%) rename src/plugins/{example_commands => example_command_plugin}/commands/help_command.py (100%) create mode 100644 src/plugins/example_command_plugin/commands/message_info_command.py create mode 100644 src/plugins/example_command_plugin/commands/send_msg_commad.py create mode 100644 src/plugins/example_command_plugin/commands/send_msg_enhanced.py create mode 100644 src/plugins/example_command_plugin/commands/send_msg_with_context.py delete mode 100644 src/plugins/example_commands/commands/echo_command.py create mode 100644 src/plugins/plugin_loader.py create mode 100644 消息发送API使用说明.md diff --git a/.gitignore b/.gitignore index 65533b856..fb46fb75c 100644 --- a/.gitignore +++ b/.gitignore @@ -309,10 +309,4 @@ src/plugins/test_plugin_pic/actions/pic_action_config.toml run_pet.bat # 忽略 /src/plugins 但保留特定目录 -/src/plugins/* -!/src/plugins/doubao_pic/ -!/src/plugins/mute_plugin/ -!/src/plugins/tts_plugin/ -!/src/plugins/vtb_action/ -!/src/plugins/__init__.py -!/src/plugins/example_commands/ +/plugins/* diff --git a/HEARFLOW_API_说明文档.md b/HEARFLOW_API_说明文档.md new file mode 100644 index 000000000..f36ee628f --- /dev/null +++ b/HEARFLOW_API_说明文档.md @@ -0,0 +1,241 @@ +# HearflowAPI 使用说明 + +## 概述 + +HearflowAPI 是一个新增的插件API模块,提供了与心流和子心流相关的操作接口。通过这个API,插件开发者可以方便地获取和操作sub_hearflow实例。 + +## 主要功能 + +### 1. 获取子心流实例 + +#### `get_sub_hearflow_by_chat_id(chat_id: str) -> Optional[SubHeartflow]` +根据chat_id获取指定的sub_hearflow实例(仅获取已存在的)。 + +**参数:** +- `chat_id`: 聊天ID,与sub_hearflow的subheartflow_id相同 + +**返回值:** +- `SubHeartflow`: sub_hearflow实例,如果不存在则返回None + +**示例:** +```python +# 获取当前聊天的子心流实例 +current_subflow = await self.get_sub_hearflow_by_chat_id(self.observation.chat_id) +if current_subflow: + print(f"找到子心流: {current_subflow.chat_id}") +else: + print("子心流不存在") +``` + +#### `get_or_create_sub_hearflow_by_chat_id(chat_id: str) -> Optional[SubHeartflow]` +根据chat_id获取或创建sub_hearflow实例。 + +**参数:** +- `chat_id`: 聊天ID + +**返回值:** +- `SubHeartflow`: sub_hearflow实例,创建失败时返回None + +**示例:** +```python +# 获取或创建子心流实例 +subflow = await self.get_or_create_sub_hearflow_by_chat_id("some_chat_id") +if subflow: + print("成功获取或创建子心流") +``` + +### 2. 获取子心流列表 + +#### `get_all_sub_hearflow_ids() -> List[str]` +获取所有活跃子心流的ID列表。 + +**返回值:** +- `List[str]`: 所有活跃子心流的ID列表 + +#### `get_all_sub_hearflows() -> List[SubHeartflow]` +获取所有活跃的子心流实例。 + +**返回值:** +- `List[SubHeartflow]`: 所有活跃的子心流实例列表 + +**示例:** +```python +# 获取所有活跃的子心流ID +all_chat_ids = self.get_all_sub_hearflow_ids() +print(f"共有 {len(all_chat_ids)} 个活跃的子心流") + +# 获取所有活跃的子心流实例 +all_subflows = self.get_all_sub_hearflows() +for subflow in all_subflows: + print(f"子心流 {subflow.chat_id} 状态: {subflow.chat_state.chat_status.value}") +``` + +### 3. 心流状态操作 + +#### `get_sub_hearflow_chat_state(chat_id: str) -> Optional[ChatState]` +获取指定子心流的聊天状态。 + +**参数:** +- `chat_id`: 聊天ID + +**返回值:** +- `ChatState`: 聊天状态,如果子心流不存在则返回None + +#### `set_sub_hearflow_chat_state(chat_id: str, target_state: ChatState) -> bool` +设置指定子心流的聊天状态。 + +**参数:** +- `chat_id`: 聊天ID +- `target_state`: 目标状态 + +**返回值:** +- `bool`: 是否设置成功 + +**示例:** +```python +from src.chat.heart_flow.sub_heartflow import ChatState + +# 获取当前状态 +current_state = await self.get_sub_hearflow_chat_state(self.observation.chat_id) +print(f"当前状态: {current_state.value}") + +# 设置状态 +success = await self.set_sub_hearflow_chat_state(self.observation.chat_id, ChatState.FOCUS) +if success: + print("状态设置成功") +``` + +### 4. Replyer和Expressor操作 + +#### `get_sub_hearflow_replyer_and_expressor(chat_id: str) -> Tuple[Optional[Any], Optional[Any]]` +根据chat_id获取指定子心流的replyer和expressor实例。 + +**参数:** +- `chat_id`: 聊天ID + +**返回值:** +- `Tuple[Optional[Any], Optional[Any]]`: (replyer实例, expressor实例),如果子心流不存在或未处于FOCUSED状态,返回(None, None) + +#### `get_sub_hearflow_replyer(chat_id: str) -> Optional[Any]` +根据chat_id获取指定子心流的replyer实例。 + +**参数:** +- `chat_id`: 聊天ID + +**返回值:** +- `Optional[Any]`: replyer实例,如果不存在则返回None + +#### `get_sub_hearflow_expressor(chat_id: str) -> Optional[Any]` +根据chat_id获取指定子心流的expressor实例。 + +**参数:** +- `chat_id`: 聊天ID + +**返回值:** +- `Optional[Any]`: expressor实例,如果不存在则返回None + +**示例:** +```python +# 获取replyer和expressor +replyer, expressor = await self.get_sub_hearflow_replyer_and_expressor(self.observation.chat_id) +if replyer and expressor: + print(f"获取到replyer: {type(replyer).__name__}") + print(f"获取到expressor: {type(expressor).__name__}") + + # 检查属性 + print(f"Replyer聊天ID: {replyer.chat_id}") + print(f"Expressor聊天ID: {expressor.chat_id}") + print(f"是否群聊: {replyer.is_group_chat}") + +# 单独获取replyer +replyer = await self.get_sub_hearflow_replyer(self.observation.chat_id) +if replyer: + print("获取到replyer实例") + +# 单独获取expressor +expressor = await self.get_sub_hearflow_expressor(self.observation.chat_id) +if expressor: + print("获取到expressor实例") +``` + +## 可用的聊天状态 + +```python +from src.chat.heart_flow.sub_heartflow import ChatState + +ChatState.FOCUS # 专注模式 +ChatState.NORMAL # 普通模式 +ChatState.ABSENT # 离开模式 +``` + +## 完整插件示例 + +```python +from typing import Tuple +from src.chat.actions.plugin_action import PluginAction, register_action +from src.chat.heart_flow.sub_heartflow import ChatState + +@register_action +class MyHearflowPlugin(PluginAction): + """我的心流插件""" + + activation_keywords = ["心流信息"] + + async def process(self) -> Tuple[bool, str]: + try: + # 获取当前聊天的chat_id + current_chat_id = self.observation.chat_id + + # 获取子心流实例 + subflow = await self.get_sub_hearflow_by_chat_id(current_chat_id) + if not subflow: + return False, "未找到子心流实例" + + # 获取状态信息 + current_state = await self.get_sub_hearflow_chat_state(current_chat_id) + + # 构建回复 + response = f"心流信息:\n" + response += f"聊天ID: {current_chat_id}\n" + response += f"当前状态: {current_state.value}\n" + response += f"是否群聊: {subflow.is_group_chat}\n" + + return True, response + + except Exception as e: + return False, f"处理出错: {str(e)}" +``` + +## 注意事项 + +1. **线程安全**: API内部已处理锁机制,确保线程安全。 + +2. **错误处理**: 所有API方法都包含异常处理,失败时会记录日志并返回安全的默认值。 + +3. **性能考虑**: `get_sub_hearflow_by_chat_id` 只获取已存在的实例,性能更好;`get_or_create_sub_hearflow_by_chat_id` 会在需要时创建新实例。 + +4. **状态管理**: 修改心流状态时请谨慎,确保不会影响系统的正常运行。 + +5. **日志记录**: 所有操作都会记录适当的日志,便于调试和监控。 + +6. **Replyer和Expressor可用性**: + - 这些实例仅在子心流处于**FOCUSED状态**时可用 + - 如果子心流处于NORMAL或ABSENT状态,将返回None + - 需要确保HeartFC实例存在且正常运行 + +7. **使用Replyer和Expressor时的注意事项**: + - 直接调用这些实例的方法需要谨慎,可能影响系统正常运行 + - 建议主要用于监控、信息获取和状态检查 + - 不建议在插件中直接调用回复生成方法,这可能与系统的正常流程冲突 + +## 相关类型和模块 + +- `SubHeartflow`: 子心流实例类 +- `ChatState`: 聊天状态枚举 +- `DefaultReplyer`: 默认回复器类 +- `DefaultExpressor`: 默认表达器类 +- `HeartFChatting`: 专注聊天主类 +- `src.chat.heart_flow.heartflow`: 主心流模块 +- `src.chat.heart_flow.subheartflow_manager`: 子心流管理器 +- `src.chat.focus_chat.replyer.default_replyer`: 回复器模块 +- `src.chat.focus_chat.expressors.default_expressor`: 表达器模块 \ No newline at end of file diff --git a/docs/plugin_loading_paths.md b/docs/plugin_loading_paths.md new file mode 100644 index 000000000..84f201f4e --- /dev/null +++ b/docs/plugin_loading_paths.md @@ -0,0 +1,119 @@ +# 插件加载路径说明 + +## 概述 + +MaiBot-Core 现在支持从多个路径加载插件,为插件开发者提供更大的灵活性。 + +## 支持的插件路径 + +系统会按以下优先级顺序搜索和加载插件: + +### 1. 项目根目录插件路径:`/plugins` +- **路径**: 项目根目录下的 `plugins/` 文件夹 +- **优先级**: 最高 +- **用途**: 用户自定义插件、第三方插件 +- **特点**: + - 与项目源码分离 + - 便于版本控制管理 + - 适合用户添加个人插件 + +### 2. 源码目录插件路径:`/src/plugins` +- **路径**: src目录下的 `plugins/` 文件夹 +- **优先级**: 次高 +- **用途**: 系统内置插件、官方插件 +- **特点**: + - 与项目源码集成 + - 适合系统级功能插件 + +## 插件结构支持 + +两个路径都支持相同的插件结构: + +### 传统结构(推荐用于复杂插件) +``` +plugins/my_plugin/ +├── __init__.py +├── actions/ +│ ├── __init__.py +│ └── my_action.py +├── commands/ +│ ├── __init__.py +│ └── my_command.py +└── config.toml +``` + +### 简化结构(推荐用于简单插件) +``` +plugins/my_plugin/ +├── __init__.py +├── my_action.py +├── my_command.py +└── config.toml +``` + +## 文件命名约定 + +### 动作文件 +- `*_action.py` +- `*_actions.py` +- 包含 `action` 字样的文件名 + +### 命令文件 +- `*_command.py` +- `*_commands.py` +- 包含 `command` 字样的文件名 + +## 加载行为 + +1. **顺序加载**: 先加载 `/plugins`,再加载 `/src/plugins` +2. **重名处理**: 如果两个路径中有同名插件,优先加载 `/plugins` 中的版本 +3. **错误隔离**: 单个插件加载失败不会影响其他插件的加载 +4. **详细日志**: 系统会记录每个插件的来源路径和加载状态 + +## 最佳实践 + +### 用户插件开发 +- 将自定义插件放在 `/plugins` 目录 +- 使用清晰的插件命名 +- 包含必要的 `__init__.py` 文件 + +### 系统插件开发 +- 将系统集成插件放在 `/src/plugins` 目录 +- 遵循项目代码规范 +- 完善的错误处理 + +### 版本控制 +- 将 `/plugins` 目录添加到 `.gitignore`(如果是用户自定义插件) +- 或者为插件创建独立的git仓库 + +## 示例插件 + +参考 `/plugins/example_root_plugin/` 中的示例插件,了解如何在根目录创建插件。 + +## 故障排除 + +### 常见问题 + +1. **插件未被加载** + - 检查插件目录是否有 `__init__.py` 文件 + - 确认文件命名符合约定 + - 查看启动日志中的加载信息 + +2. **导入错误** + - 确保插件依赖的模块已安装 + - 检查导入路径是否正确 + +3. **重复注册** + - 检查是否有同名的动作或命令 + - 避免在不同路径放置相同功能的插件 + +### 调试日志 + +启动时查看日志输出: +``` +[INFO] 正在从 plugins 加载插件... +[INFO] 正在从 src/plugins 加载插件... +[SUCCESS] 插件加载完成: 总计 X 个动作, Y 个命令 +[INFO] 插件加载详情: +[INFO] example_plugin (来源: plugins): 1 动作, 1 命令 +``` \ No newline at end of file diff --git a/src/chat/actions/plugin_action.py b/src/chat/actions/plugin_action.py index 4fc8e2245..ceda4adb8 100644 --- a/src/chat/actions/plugin_action.py +++ b/src/chat/actions/plugin_action.py @@ -17,6 +17,7 @@ from src.chat.actions.plugin_api.database_api import DatabaseAPI from src.chat.actions.plugin_api.config_api import ConfigAPI from src.chat.actions.plugin_api.utils_api import UtilsAPI from src.chat.actions.plugin_api.stream_api import StreamAPI +from src.chat.actions.plugin_api.hearflow_api import HearflowAPI # 以下为类型注解需要 from src.chat.message_receive.chat_stream import ChatStream # noqa @@ -27,7 +28,7 @@ from src.chat.focus_chat.info.obs_info import ObsInfo # noqa logger = get_logger("plugin_action") -class PluginAction(BaseAction, MessageAPI, LLMAPI, DatabaseAPI, ConfigAPI, UtilsAPI, StreamAPI): +class PluginAction(BaseAction, MessageAPI, LLMAPI, DatabaseAPI, ConfigAPI, UtilsAPI, StreamAPI, HearflowAPI): """插件动作基类 封装了主程序内部依赖,提供简化的API接口给插件开发者 diff --git a/src/chat/actions/plugin_api/__init__.py b/src/chat/actions/plugin_api/__init__.py index 3b8001e41..93c59c01e 100644 --- a/src/chat/actions/plugin_api/__init__.py +++ b/src/chat/actions/plugin_api/__init__.py @@ -4,6 +4,7 @@ from src.chat.actions.plugin_api.database_api import DatabaseAPI from src.chat.actions.plugin_api.config_api import ConfigAPI from src.chat.actions.plugin_api.utils_api import UtilsAPI from src.chat.actions.plugin_api.stream_api import StreamAPI +from src.chat.actions.plugin_api.hearflow_api import HearflowAPI __all__ = [ 'MessageAPI', @@ -12,4 +13,5 @@ __all__ = [ 'ConfigAPI', 'UtilsAPI', 'StreamAPI', + 'HearflowAPI', ] \ No newline at end of file diff --git a/src/chat/actions/plugin_api/hearflow_api.py b/src/chat/actions/plugin_api/hearflow_api.py new file mode 100644 index 000000000..c7d0452a2 --- /dev/null +++ b/src/chat/actions/plugin_api/hearflow_api.py @@ -0,0 +1,134 @@ +from typing import Optional, List, Any, Tuple +from src.common.logger_manager import get_logger +from src.chat.heart_flow.heartflow import heartflow +from src.chat.heart_flow.sub_heartflow import SubHeartflow, ChatState + +logger = get_logger("hearflow_api") + + +class HearflowAPI: + """心流API模块 + + 提供与心流和子心流相关的操作接口 + """ + + async def get_sub_hearflow_by_chat_id(self, chat_id: str) -> Optional[SubHeartflow]: + """根据chat_id获取指定的sub_hearflow实例 + + Args: + chat_id: 聊天ID,与sub_hearflow的subheartflow_id相同 + + Returns: + Optional[SubHeartflow]: sub_hearflow实例,如果不存在则返回None + """ + try: + # 直接从subheartflow_manager获取已存在的子心流 + # 使用锁来确保线程安全 + async with heartflow.subheartflow_manager._lock: + subflow = heartflow.subheartflow_manager.subheartflows.get(chat_id) + if subflow and not subflow.should_stop: + logger.debug(f"{self.log_prefix} 成功获取子心流实例: {chat_id}") + return subflow + else: + logger.debug(f"{self.log_prefix} 子心流不存在或已停止: {chat_id}") + return None + except Exception as e: + logger.error(f"{self.log_prefix} 获取子心流实例时出错: {e}") + return None + + + def get_all_sub_hearflow_ids(self) -> List[str]: + """获取所有子心流的ID列表 + + Returns: + List[str]: 所有子心流的ID列表 + """ + try: + all_subflows = heartflow.subheartflow_manager.get_all_subheartflows() + chat_ids = [subflow.chat_id for subflow in all_subflows if not subflow.should_stop] + logger.debug(f"{self.log_prefix} 获取到 {len(chat_ids)} 个活跃的子心流ID") + return chat_ids + except Exception as e: + logger.error(f"{self.log_prefix} 获取子心流ID列表时出错: {e}") + return [] + + def get_all_sub_hearflows(self) -> List[SubHeartflow]: + """获取所有子心流实例 + + Returns: + List[SubHeartflow]: 所有活跃的子心流实例列表 + """ + try: + all_subflows = heartflow.subheartflow_manager.get_all_subheartflows() + active_subflows = [subflow for subflow in all_subflows if not subflow.should_stop] + logger.debug(f"{self.log_prefix} 获取到 {len(active_subflows)} 个活跃的子心流实例") + return active_subflows + except Exception as e: + logger.error(f"{self.log_prefix} 获取子心流实例列表时出错: {e}") + return [] + + async def get_sub_hearflow_chat_state(self, chat_id: str) -> Optional[ChatState]: + """获取指定子心流的聊天状态 + + Args: + chat_id: 聊天ID + + Returns: + Optional[ChatState]: 聊天状态,如果子心流不存在则返回None + """ + try: + subflow = await self.get_sub_hearflow_by_chat_id(chat_id) + if subflow: + return subflow.chat_state.chat_status + return None + except Exception as e: + logger.error(f"{self.log_prefix} 获取子心流聊天状态时出错: {e}") + return None + + async def set_sub_hearflow_chat_state(self, chat_id: str, target_state: ChatState) -> bool: + """设置指定子心流的聊天状态 + + Args: + chat_id: 聊天ID + target_state: 目标状态 + + Returns: + bool: 是否设置成功 + """ + try: + return await heartflow.subheartflow_manager.force_change_state(chat_id, target_state) + except Exception as e: + logger.error(f"{self.log_prefix} 设置子心流聊天状态时出错: {e}") + return False + + async def get_sub_hearflow_replyer(self, chat_id: str) -> Optional[Any]: + """根据chat_id获取指定子心流的replyer实例 + + Args: + chat_id: 聊天ID + + Returns: + Optional[Any]: replyer实例,如果不存在则返回None + """ + try: + replyer, _ = await self.get_sub_hearflow_replyer_and_expressor(chat_id) + return replyer + except Exception as e: + logger.error(f"{self.log_prefix} 获取子心流replyer时出错: {e}") + return None + + async def get_sub_hearflow_expressor(self, chat_id: str) -> Optional[Any]: + """根据chat_id获取指定子心流的expressor实例 + + Args: + chat_id: 聊天ID + + Returns: + Optional[Any]: expressor实例,如果不存在则返回None + """ + try: + _, expressor = await self.get_sub_hearflow_replyer_and_expressor(chat_id) + return expressor + except Exception as e: + logger.error(f"{self.log_prefix} 获取子心流expressor时出错: {e}") + return None \ No newline at end of file diff --git a/src/chat/actions/plugin_api/message_api.py b/src/chat/actions/plugin_api/message_api.py index 38816a30e..00af27665 100644 --- a/src/chat/actions/plugin_api/message_api.py +++ b/src/chat/actions/plugin_api/message_api.py @@ -1,15 +1,22 @@ import traceback +import time from typing import Optional, List, Dict, Any from src.common.logger_manager import get_logger from src.chat.heart_flow.observation.chatting_observation import ChattingObservation from src.chat.focus_chat.hfc_utils import create_empty_anchor_message # 以下为类型注解需要 -from src.chat.message_receive.chat_stream import ChatStream +from src.chat.message_receive.chat_stream import ChatStream, chat_manager from src.chat.focus_chat.expressors.default_expressor import DefaultExpressor from src.chat.focus_chat.replyer.default_replyer import DefaultReplyer from src.chat.focus_chat.info.obs_info import ObsInfo +# 新增导入 +from src.chat.focus_chat.heartFC_sender import HeartFCSender +from src.chat.message_receive.message import MessageSending +from maim_message import Seg, UserInfo, GroupInfo +from src.config.config import global_config + logger = get_logger("message_api") class MessageAPI: @@ -18,6 +25,152 @@ class MessageAPI: 提供了发送消息、获取消息历史等功能 """ + async def send_message_to_target( + self, + message_type: str, + content: str, + platform: str, + target_id: str, + is_group: bool = True, + display_message: str = "", + ) -> bool: + """直接向指定目标发送消息 + + Args: + message_type: 消息类型,如"text"、"image"、"emoji"等 + content: 消息内容 + platform: 目标平台,如"qq" + target_id: 目标ID(群ID或用户ID) + is_group: 是否为群聊,True为群聊,False为私聊 + display_message: 显示消息(可选) + + Returns: + bool: 是否发送成功 + """ + try: + # 构建目标聊天流ID + if is_group: + # 群聊:从数据库查找对应的聊天流 + target_stream = None + for stream_id, stream in chat_manager.streams.items(): + if (stream.group_info and + str(stream.group_info.group_id) == str(target_id) and + stream.platform == platform): + target_stream = stream + break + + if not target_stream: + logger.error(f"{getattr(self, 'log_prefix', '')} 未找到群ID为 {target_id} 的聊天流") + return False + else: + # 私聊:从数据库查找对应的聊天流 + target_stream = None + for stream_id, stream in chat_manager.streams.items(): + if (not stream.group_info and + str(stream.user_info.user_id) == str(target_id) and + stream.platform == platform): + target_stream = stream + break + + if not target_stream: + logger.error(f"{getattr(self, 'log_prefix', '')} 未找到用户ID为 {target_id} 的私聊流") + return False + + # 创建HeartFCSender实例 + heart_fc_sender = HeartFCSender() + + # 生成消息ID和thinking_id + current_time = time.time() + message_id = f"plugin_msg_{int(current_time * 1000)}" + thinking_id = f"plugin_thinking_{int(current_time * 1000)}" + + # 构建机器人用户信息 + bot_user_info = UserInfo( + user_id=global_config.bot.qq_account, + user_nickname=global_config.bot.nickname, + platform=platform, + ) + + # 创建消息段 + message_segment = Seg(type=message_type, data=content) + + # 创建空锚点消息(用于回复) + anchor_message = await create_empty_anchor_message( + platform, target_stream.group_info, target_stream + ) + + # 构建发送消息对象 + bot_message = MessageSending( + message_id=message_id, + chat_stream=target_stream, + bot_user_info=bot_user_info, + sender_info=target_stream.user_info, # 目标用户信息 + message_segment=message_segment, + display_message=display_message, + reply=anchor_message, + is_head=True, + is_emoji=(message_type == "emoji"), + thinking_start_time=current_time, + ) + + # 发送消息 + sent_msg = await heart_fc_sender.send_message( + bot_message, + has_thinking=True, + typing=False, + set_reply=False + ) + + if sent_msg: + logger.info(f"{getattr(self, 'log_prefix', '')} 成功发送消息到 {platform}:{target_id}") + return True + else: + logger.error(f"{getattr(self, 'log_prefix', '')} 发送消息失败") + return False + + except Exception as e: + logger.error(f"{getattr(self, 'log_prefix', '')} 向目标发送消息时出错: {e}") + traceback.print_exc() + return False + + async def send_text_to_group(self, text: str, group_id: str, platform: str = "qq") -> bool: + """便捷方法:向指定群聊发送文本消息 + + Args: + text: 要发送的文本内容 + group_id: 群聊ID + platform: 平台,默认为"qq" + + Returns: + bool: 是否发送成功 + """ + return await self.send_message_to_target( + message_type="text", + content=text, + platform=platform, + target_id=group_id, + is_group=True + ) + + async def send_text_to_user(self, text: str, user_id: str, platform: str = "qq") -> bool: + """便捷方法:向指定用户发送私聊文本消息 + + Args: + text: 要发送的文本内容 + user_id: 用户ID + platform: 平台,默认为"qq" + + Returns: + bool: 是否发送成功 + """ + return await self.send_message_to_target( + message_type="text", + content=text, + platform=platform, + target_id=user_id, + is_group=False + ) + async def send_message(self, type: str, data: str, target: Optional[str] = "", display_message: str = "") -> bool: """发送消息的简化方法 diff --git a/src/chat/message_receive/command_handler.py b/src/chat/command/command_handler.py similarity index 71% rename from src/chat/message_receive/command_handler.py rename to src/chat/command/command_handler.py index 3cbfa0772..d15215d4d 100644 --- a/src/chat/message_receive/command_handler.py +++ b/src/chat/command/command_handler.py @@ -1,7 +1,4 @@ import re -import importlib -import pkgutil -import os from abc import ABC, abstractmethod from typing import Dict, List, Type, Optional, Tuple, Pattern from src.common.logger_manager import get_logger @@ -145,76 +142,12 @@ def register_command(cls): class CommandManager: - """命令管理器,负责加载和处理命令""" + """命令管理器,负责处理命令(不再负责加载,加载由统一的插件加载器处理)""" def __init__(self): """初始化命令管理器""" - self._load_commands() - - def _load_commands(self) -> None: - """加载所有命令""" - try: - # 检查插件目录是否存在 - plugin_path = "src.plugins" - plugin_dir = os.path.join("src", "plugins") - if not os.path.exists(plugin_dir): - logger.info(f"插件目录 {plugin_dir} 不存在,跳过插件命令加载") - return - - # 导入插件包 - try: - plugins_package = importlib.import_module(plugin_path) - logger.info(f"成功导入插件包: {plugin_path}") - except ImportError as e: - logger.error(f"导入插件包失败: {e}") - return - - # 遍历插件包中的所有子包 - loaded_commands = 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}") - - # 检查插件是否有commands子包 - plugin_commands_path = f"{plugin_name}.commands" - plugin_commands_dir = plugin_name.replace(".", os.path.sep) + os.path.sep + "commands" - - if not os.path.exists(plugin_commands_dir): - logger.debug(f"插件 {plugin_name} 没有commands目录: {plugin_commands_dir}") - continue - - try: - # 尝试导入插件的commands包 - commands_module = importlib.import_module(plugin_commands_path) - logger.info(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_commands += 1 - except Exception as e: - logger.error(f"加载命令失败: {command_module_name}, 错误: {e}") - - except ImportError as e: - logger.debug(f"插件 {plugin_name} 的commands子包导入失败: {e}") - continue - - logger.success(f"成功加载 {loaded_commands} 个插件命令") - logger.info(f"已注册的命令: {list(_COMMAND_REGISTRY.keys())}") - - except Exception as e: - logger.error(f"加载命令失败: {e}") - import traceback - logger.error(traceback.format_exc()) + # 命令加载现在由统一的插件加载器处理,这里只需要初始化 + logger.info("命令管理器初始化完成") async def process_command(self, message: MessageRecv) -> Tuple[bool, Optional[str], bool]: """处理消息中的命令 diff --git a/src/main.py b/src/main.py index ba35ce64c..a2750c803 100644 --- a/src/main.py +++ b/src/main.py @@ -26,10 +26,7 @@ import src.chat.actions.default_actions # noqa if global_config.memory.enable_memory: from .chat.memory_system.Hippocampus import hippocampus_manager -# 加载插件actions -import importlib -import pkgutil -import os +# 插件系统现在使用统一的插件加载器 install(extra_lines=3) @@ -136,70 +133,13 @@ class MainSystem: raise def _load_all_actions(self): - """加载所有actions,包括默认的和插件的""" + """加载所有actions和commands,使用统一的插件加载器""" try: - # 导入默认actions以确保装饰器被执行 + # 导入统一的插件加载器 + from src.plugins.plugin_loader import plugin_loader - # 检查插件目录是否存在 - plugin_path = "src.plugins" - plugin_dir = os.path.join("src", "plugins") - if not os.path.exists(plugin_dir): - logger.info(f"插件目录 {plugin_dir} 不存在,跳过插件动作加载") - return - - # 导入插件包 - try: - plugins_package = importlib.import_module(plugin_path) - logger.info(f"成功导入插件包: {plugin_path}") - except ImportError as e: - logger.error(f"导入插件包失败: {e}") - return - - # 遍历插件包中的所有子包 - loaded_plugins = 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}") - - # 检查插件是否有actions子包 - plugin_actions_path = f"{plugin_name}.actions" - plugin_actions_dir = plugin_name.replace(".", os.path.sep) + os.path.sep + "actions" - - if not os.path.exists(plugin_actions_dir): - logger.debug(f"插件 {plugin_name} 没有actions目录: {plugin_actions_dir}") - continue - - try: - # 尝试导入插件的actions包 - actions_module = importlib.import_module(plugin_actions_path) - logger.info(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_plugins += 1 - except Exception as e: - logger.error(f"加载动作失败: {action_module_name}, 错误: {e}") - - except ImportError as e: - logger.debug(f"插件 {plugin_name} 的actions子包导入失败: {e}") - continue - - logger.success(f"成功加载 {loaded_plugins} 个插件动作") - - except Exception as e: - logger.error(f"加载actions失败: {e}") - import traceback - logger.error(traceback.format_exc()) + # 使用统一的插件加载器加载所有插件组件 + loaded_actions, loaded_commands = plugin_loader.load_all_plugins() # 加载命令处理系统 try: @@ -210,6 +150,11 @@ class MainSystem: logger.error(f"加载命令处理系统失败: {e}") import traceback logger.error(traceback.format_exc()) + + except Exception as e: + logger.error(f"加载插件失败: {e}") + import traceback + logger.error(traceback.format_exc()) async def schedule_tasks(self): """调度定时任务""" diff --git a/src/plugins/doubao_pic/actions/pic_action.py b/src/plugins/doubao_pic/actions/pic_action.py index ffe9a15e4..8d5515366 100644 --- a/src/plugins/doubao_pic/actions/pic_action.py +++ b/src/plugins/doubao_pic/actions/pic_action.py @@ -35,7 +35,7 @@ class PicAction(PluginAction): "当有人要求你生成并发送一张图片时使用", "当有人让你画一张图时使用", ] - enable_plugin = True + enable_plugin = False action_config_file_name = "pic_action_config.toml" # 激活类型设置 diff --git a/src/plugins/example_command_plugin/README.md b/src/plugins/example_command_plugin/README.md new file mode 100644 index 000000000..4dd6bc83a --- /dev/null +++ b/src/plugins/example_command_plugin/README.md @@ -0,0 +1,105 @@ +# 发送消息命令插件 + +这个插件提供了多个便捷的消息发送命令,允许管理员向指定群聊或用户发送消息。 + +## 命令列表 + +### 1. `/send` - 基础发送命令 +向指定群聊或用户发送文本消息。 + +**语法:** +``` +/send <消息内容> +``` + +**示例:** +``` +/send group 123456789 大家好! +/send user 987654321 私聊消息 +``` + +### 2. `/sendfull` - 增强发送命令 +支持多种消息类型和平台的发送命令。 + +**语法:** +``` +/sendfull <消息类型> <目标类型> [平台] <内容> +``` + +**消息类型:** +- `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 的聊天流 +``` + +根据错误信息进行相应的处理。 \ No newline at end of file diff --git a/src/plugins/example_commands/__init__.py b/src/plugins/example_command_plugin/__init__.py similarity index 100% rename from src/plugins/example_commands/__init__.py rename to src/plugins/example_command_plugin/__init__.py diff --git a/src/plugins/example_commands/commands/__init__.py b/src/plugins/example_command_plugin/commands/__init__.py similarity index 100% rename from src/plugins/example_commands/commands/__init__.py rename to src/plugins/example_command_plugin/commands/__init__.py diff --git a/src/plugins/example_commands/commands/custom_prefix_command.py b/src/plugins/example_command_plugin/commands/custom_prefix_command.py similarity index 100% rename from src/plugins/example_commands/commands/custom_prefix_command.py rename to src/plugins/example_command_plugin/commands/custom_prefix_command.py diff --git a/src/plugins/example_commands/commands/help_command.py b/src/plugins/example_command_plugin/commands/help_command.py similarity index 100% rename from src/plugins/example_commands/commands/help_command.py rename to src/plugins/example_command_plugin/commands/help_command.py diff --git a/src/plugins/example_command_plugin/commands/message_info_command.py b/src/plugins/example_command_plugin/commands/message_info_command.py new file mode 100644 index 000000000..54c7e5062 --- /dev/null +++ b/src/plugins/example_command_plugin/commands/message_info_command.py @@ -0,0 +1,282 @@ +from src.common.logger_manager import get_logger +from src.chat.message_receive.command_handler import BaseCommand, register_command +from typing import Tuple, Optional +import json + +logger = get_logger("message_info_command") + +@register_command +class MessageInfoCommand(BaseCommand): + """消息信息查看命令,展示发送命令的原始消息和相关信息""" + + command_name = "msginfo" + command_description = "查看发送命令的原始消息信息" + command_pattern = r"^/msginfo(?:\s+(?Pfull|simple))?$" + command_help = "使用方法: /msginfo [full|simple] - 查看当前消息的详细信息" + command_examples = ["/msginfo", "/msginfo full", "/msginfo simple"] + enable_command = True + + async def execute(self) -> Tuple[bool, Optional[str]]: + """执行消息信息查看命令""" + try: + detail_level = self.matched_groups.get("detail", "simple") + + logger.info(f"{self.log_prefix} 查看消息信息,详细级别: {detail_level}") + + if detail_level == "full": + info_text = self._get_full_message_info() + else: + info_text = self._get_simple_message_info() + + return True, info_text + + except Exception as e: + logger.error(f"{self.log_prefix} 获取消息信息时出错: {e}") + return False, f"获取消息信息失败: {str(e)}" + + def _get_simple_message_info(self) -> str: + """获取简化的消息信息""" + message = self.message + + # 基础信息 + info_lines = [ + "📨 消息信息概览", + f"🆔 消息ID: {message.message_info.message_id}", + f"⏰ 时间: {message.message_info.time}", + f"🌐 平台: {message.message_info.platform}", + ] + + # 发送者信息 + user = message.message_info.user_info + info_lines.extend([ + "", + "👤 发送者信息:", + f" 用户ID: {user.user_id}", + f" 昵称: {user.user_nickname}", + f" 群名片: {user.user_cardname or '无'}", + ]) + + # 群聊信息(如果是群聊) + if message.message_info.group_info: + group = message.message_info.group_info + info_lines.extend([ + "", + "👥 群聊信息:", + f" 群ID: {group.group_id}", + f" 群名: {group.group_name or '未知'}", + ]) + else: + info_lines.extend([ + "", + "💬 消息类型: 私聊消息", + ]) + + # 消息内容 + info_lines.extend([ + "", + "📝 消息内容:", + f" 原始文本: {message.processed_plain_text}", + f" 是否表情: {'是' if getattr(message, 'is_emoji', False) else '否'}", + ]) + + # 聊天流信息 + if hasattr(message, 'chat_stream') and message.chat_stream: + chat_stream = message.chat_stream + info_lines.extend([ + "", + "🔄 聊天流信息:", + f" 流ID: {chat_stream.stream_id}", + f" 是否激活: {'是' if chat_stream.is_active else '否'}", + ]) + + return "\n".join(info_lines) + + def _get_full_message_info(self) -> str: + """获取完整的消息信息(包含技术细节)""" + message = self.message + + info_lines = [ + "📨 完整消息信息", + "=" * 40, + ] + + # 消息基础信息 + info_lines.extend([ + "", + "🔍 基础消息信息:", + f" 消息ID: {message.message_info.message_id}", + f" 时间戳: {message.message_info.time}", + f" 平台: {message.message_info.platform}", + f" 处理后文本: {message.processed_plain_text}", + f" 详细文本: {message.detailed_plain_text[:100]}{'...' if len(message.detailed_plain_text) > 100 else ''}", + ]) + + # 用户详细信息 + user = message.message_info.user_info + info_lines.extend([ + "", + "👤 发送者详细信息:", + f" 用户ID: {user.user_id}", + f" 昵称: {user.user_nickname}", + f" 群名片: {user.user_cardname or '无'}", + f" 平台: {user.platform}", + ]) + + # 群聊详细信息 + if message.message_info.group_info: + group = message.message_info.group_info + info_lines.extend([ + "", + "👥 群聊详细信息:", + f" 群ID: {group.group_id}", + f" 群名: {group.group_name or '未知'}", + f" 平台: {group.platform}", + ]) + else: + info_lines.append("\n💬 消息类型: 私聊消息") + + # 消息段信息 + if message.message_segment: + info_lines.extend([ + "", + "📦 消息段信息:", + f" 类型: {message.message_segment.type}", + f" 数据类型: {type(message.message_segment.data).__name__}", + f" 数据预览: {str(message.message_segment.data)[:200]}{'...' if len(str(message.message_segment.data)) > 200 else ''}", + ]) + + # 聊天流详细信息 + if hasattr(message, 'chat_stream') and message.chat_stream: + chat_stream = message.chat_stream + info_lines.extend([ + "", + "🔄 聊天流详细信息:", + f" 流ID: {chat_stream.stream_id}", + f" 平台: {chat_stream.platform}", + f" 是否激活: {'是' if chat_stream.is_active else '否'}", + f" 用户信息: {chat_stream.user_info.user_nickname} ({chat_stream.user_info.user_id})", + f" 群信息: {getattr(chat_stream.group_info, 'group_name', '私聊') if chat_stream.group_info else '私聊'}", + ]) + + # 回复信息 + if hasattr(message, 'reply') and message.reply: + info_lines.extend([ + "", + "↩️ 回复信息:", + f" 回复消息ID: {message.reply.message_info.message_id}", + f" 回复内容: {message.reply.processed_plain_text[:100]}{'...' if len(message.reply.processed_plain_text) > 100 else ''}", + ]) + + # 原始消息数据(如果存在) + if hasattr(message, 'raw_message') and message.raw_message: + info_lines.extend([ + "", + "🗂️ 原始消息数据:", + f" 数据类型: {type(message.raw_message).__name__}", + f" 数据大小: {len(str(message.raw_message))} 字符", + ]) + + return "\n".join(info_lines) + + +@register_command +class SenderInfoCommand(BaseCommand): + """发送者信息命令,快速查看发送者信息""" + + command_name = "whoami" + command_description = "查看发送命令的用户信息" + command_pattern = r"^/whoami$" + command_help = "使用方法: /whoami - 查看你的用户信息" + command_examples = ["/whoami"] + enable_command = True + + async def execute(self) -> Tuple[bool, Optional[str]]: + """执行发送者信息查看命令""" + try: + user = self.message.message_info.user_info + group = self.message.message_info.group_info + + info_lines = [ + "👤 你的身份信息", + f"🆔 用户ID: {user.user_id}", + f"📝 昵称: {user.user_nickname}", + f"🏷️ 群名片: {user.user_cardname or '无'}", + f"🌐 平台: {user.platform}", + ] + + if group: + info_lines.extend([ + "", + "👥 当前群聊:", + f"🆔 群ID: {group.group_id}", + f"📝 群名: {group.group_name or '未知'}", + ]) + else: + info_lines.append("\n💬 当前在私聊中") + + return True, "\n".join(info_lines) + + except Exception as e: + logger.error(f"{self.log_prefix} 获取发送者信息时出错: {e}") + return False, f"获取发送者信息失败: {str(e)}" + + +@register_command +class ChatStreamInfoCommand(BaseCommand): + """聊天流信息命令""" + + command_name = "streaminfo" + command_description = "查看当前聊天流的详细信息" + command_pattern = r"^/streaminfo$" + command_help = "使用方法: /streaminfo - 查看当前聊天流信息" + command_examples = ["/streaminfo"] + enable_command = True + + async def execute(self) -> Tuple[bool, Optional[str]]: + """执行聊天流信息查看命令""" + try: + if not hasattr(self.message, 'chat_stream') or not self.message.chat_stream: + return False, "无法获取聊天流信息" + + chat_stream = self.message.chat_stream + + info_lines = [ + "🔄 聊天流信息", + f"🆔 流ID: {chat_stream.stream_id}", + f"🌐 平台: {chat_stream.platform}", + f"⚡ 状态: {'激活' if chat_stream.is_active else '非激活'}", + ] + + # 用户信息 + if chat_stream.user_info: + info_lines.extend([ + "", + "👤 关联用户:", + f" ID: {chat_stream.user_info.user_id}", + f" 昵称: {chat_stream.user_info.user_nickname}", + ]) + + # 群信息 + if chat_stream.group_info: + info_lines.extend([ + "", + "👥 关联群聊:", + f" 群ID: {chat_stream.group_info.group_id}", + f" 群名: {chat_stream.group_info.group_name or '未知'}", + ]) + else: + info_lines.append("\n💬 类型: 私聊流") + + # 最近消息统计 + if hasattr(chat_stream, 'last_messages'): + msg_count = len(chat_stream.last_messages) + info_lines.extend([ + "", + f"📈 消息统计: 记录了 {msg_count} 条最近消息", + ]) + + return True, "\n".join(info_lines) + + except Exception as e: + logger.error(f"{self.log_prefix} 获取聊天流信息时出错: {e}") + return False, f"获取聊天流信息失败: {str(e)}" \ No newline at end of file diff --git a/src/plugins/example_command_plugin/commands/send_msg_commad.py b/src/plugins/example_command_plugin/commands/send_msg_commad.py new file mode 100644 index 000000000..bbc0cc50d --- /dev/null +++ b/src/plugins/example_command_plugin/commands/send_msg_commad.py @@ -0,0 +1,119 @@ +from src.common.logger_manager import get_logger +from src.chat.message_receive.command_handler import BaseCommand, register_command +from src.chat.actions.plugin_api.message_api import MessageAPI +from typing import Tuple, Optional + +logger = get_logger("send_msg_command") + +@register_command +class SendMessageCommand(BaseCommand, MessageAPI): + """发送消息命令,可以向指定群聊或私聊发送消息""" + + command_name = "send" + command_description = "向指定群聊或私聊发送消息" + command_pattern = r"^/send\s+(?Pgroup|user)\s+(?P\d+)\s+(?P.+)$" + command_help = "使用方法: /send <消息内容> - 发送消息到指定群聊或用户" + command_examples = [ + "/send group 123456789 大家好!", + "/send user 987654321 私聊消息" + ] + enable_command = True + + def __init__(self, message): + super().__init__(message) + # 初始化MessageAPI需要的服务(虽然这里不会用到,但保持一致性) + self._services = {} + self.log_prefix = f"[Command:{self.command_name}]" + + async def execute(self) -> Tuple[bool, Optional[str]]: + """执行发送消息命令 + + Returns: + Tuple[bool, Optional[str]]: (是否执行成功, 回复消息) + """ + try: + # 获取匹配到的参数 + target_type = self.matched_groups.get("target_type") # group 或 user + target_id = self.matched_groups.get("target_id") # 群ID或用户ID + content = self.matched_groups.get("content") # 消息内容 + + if not all([target_type, target_id, content]): + return False, "命令参数不完整,请检查格式" + + logger.info(f"{self.log_prefix} 执行发送消息命令: {target_type}:{target_id} -> {content[:50]}...") + + # 根据目标类型调用不同的发送方法 + if target_type == "group": + success = await self._send_to_group(target_id, content) + target_desc = f"群聊 {target_id}" + elif target_type == "user": + success = await self._send_to_user(target_id, content) + target_desc = f"用户 {target_id}" + else: + return False, f"不支持的目标类型: {target_type},只支持 group 或 user" + + # 返回执行结果 + if success: + return True, f"✅ 消息已成功发送到 {target_desc}" + else: + return False, f"❌ 消息发送失败,可能是目标 {target_desc} 不存在或没有权限" + + except Exception as e: + logger.error(f"{self.log_prefix} 执行发送消息命令时出错: {e}") + return False, f"命令执行出错: {str(e)}" + + async def _send_to_group(self, group_id: str, content: str) -> bool: + """发送消息到群聊 + + Args: + group_id: 群聊ID + content: 消息内容 + + Returns: + bool: 是否发送成功 + """ + try: + success = await self.send_text_to_group( + text=content, + group_id=group_id, + platform="qq" # 默认使用QQ平台 + ) + + if success: + logger.info(f"{self.log_prefix} 成功发送消息到群聊 {group_id}") + else: + logger.warning(f"{self.log_prefix} 发送消息到群聊 {group_id} 失败") + + return success + + except Exception as e: + logger.error(f"{self.log_prefix} 发送群聊消息时出错: {e}") + return False + + async def _send_to_user(self, user_id: str, content: str) -> bool: + """发送消息到私聊 + + Args: + user_id: 用户ID + content: 消息内容 + + Returns: + bool: 是否发送成功 + """ + try: + success = await self.send_text_to_user( + text=content, + user_id=user_id, + platform="qq" # 默认使用QQ平台 + ) + + if success: + logger.info(f"{self.log_prefix} 成功发送消息到用户 {user_id}") + else: + logger.warning(f"{self.log_prefix} 发送消息到用户 {user_id} 失败") + + return success + + except Exception as e: + logger.error(f"{self.log_prefix} 发送私聊消息时出错: {e}") + return False \ No newline at end of file diff --git a/src/plugins/example_command_plugin/commands/send_msg_enhanced.py b/src/plugins/example_command_plugin/commands/send_msg_enhanced.py new file mode 100644 index 000000000..6b479eb9d --- /dev/null +++ b/src/plugins/example_command_plugin/commands/send_msg_enhanced.py @@ -0,0 +1,170 @@ +from src.common.logger_manager import get_logger +from src.chat.message_receive.command_handler import BaseCommand, register_command +from src.chat.actions.plugin_api.message_api import MessageAPI +from typing import Tuple, Optional + +logger = get_logger("send_msg_enhanced") + +@register_command +class SendMessageEnhancedCommand(BaseCommand, MessageAPI): + """增强版发送消息命令,支持多种消息类型和平台""" + + command_name = "sendfull" + command_description = "增强版消息发送命令,支持多种类型和平台" + command_pattern = r"^/sendfull\s+(?Ptext|image|emoji)\s+(?Pgroup|user)\s+(?P\d+)(?:\s+(?P\w+))?\s+(?P.+)$" + command_help = "使用方法: /sendfull <消息类型> <目标类型> [平台] <内容>" + command_examples = [ + "/sendfull text group 123456789 qq 大家好!这是文本消息", + "/sendfull image user 987654321 https://example.com/image.jpg", + "/sendfull emoji group 123456789 😄", + "/sendfull text user 987654321 qq 私聊消息" + ] + enable_command = True + + def __init__(self, message): + super().__init__(message) + self._services = {} + self.log_prefix = f"[Command:{self.command_name}]" + + async def execute(self) -> Tuple[bool, Optional[str]]: + """执行增强版发送消息命令""" + try: + # 获取匹配参数 + msg_type = self.matched_groups.get("msg_type") # 消息类型: text/image/emoji + target_type = self.matched_groups.get("target_type") # 目标类型: group/user + target_id = self.matched_groups.get("target_id") # 目标ID + platform = self.matched_groups.get("platform") or "qq" # 平台,默认qq + content = self.matched_groups.get("content") # 内容 + + if not all([msg_type, target_type, target_id, content]): + return False, "命令参数不完整,请检查格式" + + # 验证消息类型 + valid_types = ["text", "image", "emoji"] + if msg_type not in valid_types: + return False, f"不支持的消息类型: {msg_type},支持的类型: {', '.join(valid_types)}" + + # 验证目标类型 + if target_type not in ["group", "user"]: + return False, "目标类型只能是 group 或 user" + + logger.info(f"{self.log_prefix} 执行发送命令: {msg_type} -> {target_type}:{target_id} (平台:{platform})") + + # 根据消息类型和目标类型发送消息 + is_group = (target_type == "group") + success = await self.send_message_to_target( + message_type=msg_type, + content=content, + platform=platform, + target_id=target_id, + is_group=is_group + ) + + # 构建结果消息 + target_desc = f"{'群聊' if is_group else '用户'} {target_id} (平台: {platform})" + msg_type_desc = { + "text": "文本", + "image": "图片", + "emoji": "表情" + }.get(msg_type, msg_type) + + if success: + return True, f"✅ {msg_type_desc}消息已成功发送到 {target_desc}" + else: + return False, f"❌ {msg_type_desc}消息发送失败,可能是目标 {target_desc} 不存在或没有权限" + + except Exception as e: + logger.error(f"{self.log_prefix} 执行增强发送命令时出错: {e}") + return False, f"命令执行出错: {str(e)}" + + +@register_command +class SendQuickCommand(BaseCommand, MessageAPI): + """快速发送文本消息命令""" + + command_name = "msg" + command_description = "快速发送文本消息到群聊" + command_pattern = r"^/msg\s+(?P\d+)\s+(?P.+)$" + command_help = "使用方法: /msg <群ID> <消息内容> - 快速发送文本到指定群聊" + command_examples = [ + "/msg 123456789 大家好!", + "/msg 987654321 这是一条快速消息" + ] + enable_command = True + + def __init__(self, message): + super().__init__(message) + self._services = {} + self.log_prefix = f"[Command:{self.command_name}]" + + async def execute(self) -> Tuple[bool, Optional[str]]: + """执行快速发送消息命令""" + try: + group_id = self.matched_groups.get("group_id") + content = self.matched_groups.get("content") + + if not all([group_id, content]): + return False, "命令参数不完整" + + logger.info(f"{self.log_prefix} 快速发送到群 {group_id}: {content[:50]}...") + + success = await self.send_text_to_group( + text=content, + group_id=group_id, + platform="qq" + ) + + if success: + return True, f"✅ 消息已发送到群 {group_id}" + else: + return False, f"❌ 发送到群 {group_id} 失败" + + except Exception as e: + logger.error(f"{self.log_prefix} 快速发送命令出错: {e}") + return False, f"发送失败: {str(e)}" + + +@register_command +class SendPrivateCommand(BaseCommand, MessageAPI): + """发送私聊消息命令""" + + command_name = "pm" + command_description = "发送私聊消息到指定用户" + command_pattern = r"^/pm\s+(?P\d+)\s+(?P.+)$" + command_help = "使用方法: /pm <用户ID> <消息内容> - 发送私聊消息" + command_examples = [ + "/pm 123456789 你好!", + "/pm 987654321 这是私聊消息" + ] + enable_command = True + + def __init__(self, message): + super().__init__(message) + self._services = {} + self.log_prefix = f"[Command:{self.command_name}]" + + async def execute(self) -> Tuple[bool, Optional[str]]: + """执行私聊发送命令""" + try: + user_id = self.matched_groups.get("user_id") + content = self.matched_groups.get("content") + + if not all([user_id, content]): + return False, "命令参数不完整" + + logger.info(f"{self.log_prefix} 发送私聊到用户 {user_id}: {content[:50]}...") + + success = await self.send_text_to_user( + text=content, + user_id=user_id, + platform="qq" + ) + + if success: + return True, f"✅ 私聊消息已发送到用户 {user_id}" + else: + return False, f"❌ 发送私聊到用户 {user_id} 失败" + + except Exception as e: + logger.error(f"{self.log_prefix} 私聊发送命令出错: {e}") + return False, f"私聊发送失败: {str(e)}" \ No newline at end of file diff --git a/src/plugins/example_command_plugin/commands/send_msg_with_context.py b/src/plugins/example_command_plugin/commands/send_msg_with_context.py new file mode 100644 index 000000000..16b54e97b --- /dev/null +++ b/src/plugins/example_command_plugin/commands/send_msg_with_context.py @@ -0,0 +1,253 @@ +from src.common.logger_manager import get_logger +from src.chat.message_receive.command_handler import BaseCommand, register_command +from src.chat.actions.plugin_api.message_api import MessageAPI +from typing import Tuple, Optional +import time + +logger = get_logger("send_msg_with_context") + +@register_command +class ContextAwareSendCommand(BaseCommand, MessageAPI): + """上下文感知的发送消息命令,展示如何利用原始消息信息""" + + command_name = "csend" + command_description = "带上下文感知的发送消息命令" + command_pattern = r"^/csend\s+(?Pgroup|user|here|reply)\s+(?P.*?)(?:\s+(?P.*))?$" + command_help = "使用方法: /csend <参数> [内容]" + command_examples = [ + "/csend group 123456789 大家好!", + "/csend user 987654321 私聊消息", + "/csend here 在当前聊天发送", + "/csend reply 回复当前群/私聊" + ] + enable_command = True + + # 管理员用户ID列表(示例) + ADMIN_USERS = ["123456789", "987654321"] # 可以从配置文件读取 + + def __init__(self, message): + super().__init__(message) + self._services = {} + self.log_prefix = f"[Command:{self.command_name}]" + + async def execute(self) -> Tuple[bool, Optional[str]]: + """执行上下文感知的发送命令""" + try: + # 获取命令发送者信息 + sender = self.message.message_info.user_info + current_group = self.message.message_info.group_info + + # 权限检查 + if not self._check_permission(sender.user_id): + return False, f"❌ 权限不足,只有管理员可以使用此命令\n你的ID: {sender.user_id}" + + # 解析命令参数 + target_type = self.matched_groups.get("target_type") + target_id_or_content = self.matched_groups.get("target_id_or_content", "") + content = self.matched_groups.get("content", "") + + # 根据目标类型处理不同情况 + if target_type == "here": + # 发送到当前聊天 + return await self._send_to_current_chat(target_id_or_content, sender, current_group) + + elif target_type == "reply": + # 回复到当前聊天,带发送者信息 + return await self._send_reply_with_context(target_id_or_content, sender, current_group) + + elif target_type in ["group", "user"]: + # 发送到指定目标 + if not content: + return False, "指定群聊或用户时需要提供消息内容" + return await self._send_to_target(target_type, target_id_or_content, content, sender) + + else: + return False, f"不支持的目标类型: {target_type}" + + except Exception as e: + logger.error(f"{self.log_prefix} 执行上下文感知发送命令时出错: {e}") + return False, f"命令执行出错: {str(e)}" + + def _check_permission(self, user_id: str) -> bool: + """检查用户权限""" + return user_id in self.ADMIN_USERS + + async def _send_to_current_chat(self, content: str, sender, current_group) -> Tuple[bool, str]: + """发送到当前聊天""" + if not content: + return False, "消息内容不能为空" + + # 构建带发送者信息的消息 + timestamp = time.strftime("%H:%M:%S", time.localtime()) + if current_group: + # 群聊 + formatted_content = f"[管理员转发 {timestamp}] {sender.user_nickname}({sender.user_id}): {content}" + success = await self.send_text_to_group( + text=formatted_content, + group_id=current_group.group_id, + platform="qq" + ) + target_desc = f"当前群聊 {current_group.group_name}({current_group.group_id})" + else: + # 私聊 + formatted_content = f"[管理员消息 {timestamp}]: {content}" + success = await self.send_text_to_user( + text=formatted_content, + user_id=sender.user_id, + platform="qq" + ) + target_desc = "当前私聊" + + if success: + return True, f"✅ 消息已发送到{target_desc}" + else: + return False, f"❌ 发送到{target_desc}失败" + + async def _send_reply_with_context(self, content: str, sender, current_group) -> Tuple[bool, str]: + """发送回复,带完整上下文信息""" + if not content: + return False, "回复内容不能为空" + + # 获取当前时间和环境信息 + timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + + # 构建上下文信息 + context_info = [ + f"📢 管理员回复 [{timestamp}]", + f"👤 发送者: {sender.user_nickname}({sender.user_id})", + ] + + if current_group: + context_info.append(f"👥 当前群聊: {current_group.group_name}({current_group.group_id})") + target_desc = f"群聊 {current_group.group_name}" + else: + context_info.append("💬 当前环境: 私聊") + target_desc = "私聊" + + context_info.extend([ + f"📝 回复内容: {content}", + "─" * 30 + ]) + + formatted_content = "\n".join(context_info) + + # 发送消息 + if current_group: + success = await self.send_text_to_group( + text=formatted_content, + group_id=current_group.group_id, + platform="qq" + ) + else: + success = await self.send_text_to_user( + text=formatted_content, + user_id=sender.user_id, + platform="qq" + ) + + if success: + return True, f"✅ 带上下文的回复已发送到{target_desc}" + else: + return False, f"❌ 发送上下文回复到{target_desc}失败" + + async def _send_to_target(self, target_type: str, target_id: str, content: str, sender) -> Tuple[bool, str]: + """发送到指定目标,带发送者追踪信息""" + timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + + # 构建带追踪信息的消息 + tracking_info = f"[管理转发 {timestamp}] 来自 {sender.user_nickname}({sender.user_id})" + formatted_content = f"{tracking_info}\n{content}" + + if target_type == "group": + success = await self.send_text_to_group( + text=formatted_content, + group_id=target_id, + platform="qq" + ) + target_desc = f"群聊 {target_id}" + else: # user + success = await self.send_text_to_user( + text=formatted_content, + user_id=target_id, + platform="qq" + ) + target_desc = f"用户 {target_id}" + + if success: + return True, f"✅ 带追踪信息的消息已发送到{target_desc}" + else: + return False, f"❌ 发送到{target_desc}失败" + + +@register_command +class MessageContextCommand(BaseCommand): + """消息上下文命令,展示如何获取和利用上下文信息""" + + command_name = "context" + command_description = "显示当前消息的完整上下文信息" + command_pattern = r"^/context$" + command_help = "使用方法: /context - 显示当前环境的上下文信息" + command_examples = ["/context"] + enable_command = True + + async def execute(self) -> Tuple[bool, Optional[str]]: + """显示上下文信息""" + try: + message = self.message + user = message.message_info.user_info + group = message.message_info.group_info + + # 构建上下文信息 + context_lines = [ + "🌐 当前上下文信息", + "=" * 30, + "", + "⏰ 时间信息:", + f" 消息时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(message.message_info.time))}", + f" 时间戳: {message.message_info.time}", + "", + "👤 发送者:", + f" 用户ID: {user.user_id}", + f" 昵称: {user.user_nickname}", + f" 群名片: {user.user_cardname or '无'}", + f" 平台: {user.platform}", + ] + + if group: + context_lines.extend([ + "", + "👥 群聊环境:", + f" 群ID: {group.group_id}", + f" 群名: {group.group_name or '未知'}", + f" 平台: {group.platform}", + ]) + else: + context_lines.extend([ + "", + "💬 私聊环境", + ]) + + # 添加聊天流信息 + if hasattr(message, 'chat_stream') and message.chat_stream: + chat_stream = message.chat_stream + context_lines.extend([ + "", + "🔄 聊天流:", + f" 流ID: {chat_stream.stream_id}", + f" 激活状态: {'激活' if chat_stream.is_active else '非激活'}", + ]) + + # 添加消息内容信息 + context_lines.extend([ + "", + "📝 消息内容:", + f" 原始内容: {message.processed_plain_text}", + f" 消息长度: {len(message.processed_plain_text)} 字符", + f" 消息ID: {message.message_info.message_id}", + ]) + + return True, "\n".join(context_lines) + + except Exception as e: + logger.error(f"{self.log_prefix} 获取上下文信息时出错: {e}") + return False, f"获取上下文失败: {str(e)}" \ No newline at end of file diff --git a/src/plugins/example_commands/commands/echo_command.py b/src/plugins/example_commands/commands/echo_command.py deleted file mode 100644 index 7db731cbf..000000000 --- a/src/plugins/example_commands/commands/echo_command.py +++ /dev/null @@ -1,36 +0,0 @@ -from src.common.logger_manager import get_logger -from src.chat.message_receive.command_handler import BaseCommand, register_command -from typing import Tuple, Optional - -logger = get_logger("echo_command") - -@register_command -class EchoCommand(BaseCommand): - """回显命令,将用户输入的内容回显""" - - command_name = "echo" - command_description = "回显命令,将用户输入的内容回显" - command_pattern = r"^/echo\s+(?P.+)$" # 匹配 /echo 后面的所有内容 - command_help = "使用方法: /echo <内容> - 回显你输入的内容" - command_examples = ["/echo 你好,世界!", "/echo 这是一个测试"] - enable_command = True - - async def execute(self) -> Tuple[bool, Optional[str]]: - """执行回显命令 - - Returns: - Tuple[bool, Optional[str]]: (是否执行成功, 回复消息) - """ - try: - # 获取匹配到的内容 - content = self.matched_groups.get("content") - - if not content: - return False, "请提供要回显的内容" - - logger.info(f"{self.log_prefix} 执行回显命令: {content}") - return True, f"🔄 {content}" - - except Exception as e: - logger.error(f"{self.log_prefix} 执行回显命令时出错: {e}") - return False, f"执行命令时出错: {str(e)}" \ No newline at end of file diff --git a/src/plugins/plugin_loader.py b/src/plugins/plugin_loader.py new file mode 100644 index 000000000..7779c1307 --- /dev/null +++ b/src/plugins/plugin_loader.py @@ -0,0 +1,303 @@ +import importlib +import pkgutil +import os +from typing import Dict, List, 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() \ No newline at end of file diff --git a/消息发送API使用说明.md b/消息发送API使用说明.md new file mode 100644 index 000000000..c1d25e34f --- /dev/null +++ b/消息发送API使用说明.md @@ -0,0 +1,129 @@ +# 消息发送API使用说明 + +## 概述 + +新的消息发送API允许插件直接向指定的平台和ID发送消息,无需依赖当前聊天上下文。API会自动从数据库中匹配chat_stream并构建相应的发送消息对象。 + +## 可用方法 + +### 1. `send_message_to_target()` + +最通用的消息发送方法,支持各种类型的消息。 + +```python +async def send_message_to_target( + self, + message_type: str, # 消息类型:text, image, emoji等 + content: str, # 消息内容 + platform: str, # 目标平台:qq等 + target_id: str, # 目标ID(群ID或用户ID) + is_group: bool = True, # 是否为群聊 + display_message: str = "", # 显示消息(可选) +) -> bool: +``` + +**示例用法:** +```python +# 发送文本消息到群聊 +success = await self.send_message_to_target( + message_type="text", + content="Hello, 这是一条测试消息!", + platform="qq", + target_id="123456789", + is_group=True +) + +# 发送图片到私聊 +success = await self.send_message_to_target( + message_type="image", + content="https://example.com/image.jpg", + platform="qq", + target_id="987654321", + is_group=False +) + +# 发送表情包 +success = await self.send_message_to_target( + message_type="emoji", + content="😄", + platform="qq", + target_id="123456789", + is_group=True +) +``` + +### 2. `send_text_to_group()` + +便捷方法,专门用于向群聊发送文本消息。 + +```python +async def send_text_to_group( + self, + text: str, # 文本内容 + group_id: str, # 群聊ID + platform: str = "qq" # 平台,默认为qq +) -> bool: +``` + +**示例用法:** +```python +success = await self.send_text_to_group( + text="群聊测试消息", + group_id="123456789" +) +``` + +### 3. `send_text_to_user()` + +便捷方法,专门用于向用户发送私聊文本消息。 + +```python +async def send_text_to_user( + self, + text: str, # 文本内容 + user_id: str, # 用户ID + platform: str = "qq" # 平台,默认为qq +) -> bool: +``` + +**示例用法:** +```python +success = await self.send_text_to_user( + text="私聊测试消息", + user_id="987654321" +) +``` + +## 支持的消息类型 + +- `"text"` - 文本消息 +- `"image"` - 图片消息(需要提供图片URL或路径) +- `"emoji"` - 表情消息 +- `"voice"` - 语音消息 +- `"video"` - 视频消息 +- 其他类型根据平台支持情况 + +## 注意事项 + +1. **前提条件**:目标群聊或用户必须已经在数据库中存在对应的chat_stream记录 +2. **权限要求**:机器人必须在目标群聊中有发言权限 +3. **错误处理**:所有方法都会返回bool值表示发送成功与否,同时会在日志中记录详细错误信息 +4. **异步调用**:所有方法都是异步的,需要使用`await`调用 + +## 完整示例插件 + +参考 `example_send_message_plugin.py` 文件,该文件展示了如何在插件中使用新的消息发送API。 + +## 配置文件支持 + +可以通过TOML配置文件管理目标ID、默认平台等设置。参考 `example_config.toml` 文件。 + +## 错误排查 + +如果消息发送失败,请检查: + +1. 目标ID是否正确 +2. chat_stream是否已加载到ChatManager中 +3. 机器人是否有相应权限 +4. 网络连接是否正常 +5. 查看日志中的详细错误信息 \ No newline at end of file