feat:统一加载插件,区分内部插件和外部插件,提供示例命令发送插件

This commit is contained in:
SengokuCola
2025-06-09 23:51:12 +08:00
parent 450dad2355
commit c44811815a
22 changed files with 2029 additions and 182 deletions

8
.gitignore vendored
View File

@@ -309,10 +309,4 @@ src/plugins/test_plugin_pic/actions/pic_action_config.toml
run_pet.bat run_pet.bat
# 忽略 /src/plugins 但保留特定目录 # 忽略 /src/plugins 但保留特定目录
/src/plugins/* /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/

View File

@@ -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`: 表达器模块

View File

@@ -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 命令
```

View File

@@ -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.config_api import ConfigAPI
from src.chat.actions.plugin_api.utils_api import UtilsAPI 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.stream_api import StreamAPI
from src.chat.actions.plugin_api.hearflow_api import HearflowAPI
# 以下为类型注解需要 # 以下为类型注解需要
from src.chat.message_receive.chat_stream import ChatStream # noqa 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") 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接口给插件开发者 封装了主程序内部依赖提供简化的API接口给插件开发者

View File

@@ -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.config_api import ConfigAPI
from src.chat.actions.plugin_api.utils_api import UtilsAPI 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.stream_api import StreamAPI
from src.chat.actions.plugin_api.hearflow_api import HearflowAPI
__all__ = [ __all__ = [
'MessageAPI', 'MessageAPI',
@@ -12,4 +13,5 @@ __all__ = [
'ConfigAPI', 'ConfigAPI',
'UtilsAPI', 'UtilsAPI',
'StreamAPI', 'StreamAPI',
'HearflowAPI',
] ]

View File

@@ -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

View File

@@ -1,15 +1,22 @@
import traceback import traceback
import time
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation 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.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.expressors.default_expressor import DefaultExpressor
from src.chat.focus_chat.replyer.default_replyer import DefaultReplyer 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.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") logger = get_logger("message_api")
class MessageAPI: 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: async def send_message(self, type: str, data: str, target: Optional[str] = "", display_message: str = "") -> bool:
"""发送消息的简化方法 """发送消息的简化方法

View File

@@ -1,7 +1,4 @@
import re import re
import importlib
import pkgutil
import os
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, List, Type, Optional, Tuple, Pattern from typing import Dict, List, Type, Optional, Tuple, Pattern
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
@@ -145,76 +142,12 @@ def register_command(cls):
class CommandManager: class CommandManager:
"""命令管理器,负责加载和处理命令""" """命令管理器,负责处理命令(不再负责加载,加载由统一的插件加载器处理)"""
def __init__(self): def __init__(self):
"""初始化命令管理器""" """初始化命令管理器"""
self._load_commands() # 命令加载现在由统一的插件加载器处理,这里只需要初始化
logger.info("命令管理器初始化完成")
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())
async def process_command(self, message: MessageRecv) -> Tuple[bool, Optional[str], bool]: async def process_command(self, message: MessageRecv) -> Tuple[bool, Optional[str], bool]:
"""处理消息中的命令 """处理消息中的命令

View File

@@ -26,10 +26,7 @@ import src.chat.actions.default_actions # noqa
if global_config.memory.enable_memory: if global_config.memory.enable_memory:
from .chat.memory_system.Hippocampus import hippocampus_manager from .chat.memory_system.Hippocampus import hippocampus_manager
# 加载插件actions # 插件系统现在使用统一的插件加载器
import importlib
import pkgutil
import os
install(extra_lines=3) install(extra_lines=3)
@@ -136,70 +133,13 @@ class MainSystem:
raise raise
def _load_all_actions(self): def _load_all_actions(self):
"""加载所有actions,包括默认的和插件的""" """加载所有actions和commands使用统一的插件加载器"""
try: try:
# 导入默认actions以确保装饰器被执行 # 导入统一的插件加载器
from src.plugins.plugin_loader import plugin_loader
# 检查插件目录是否存在 # 使用统一的插件加载器加载所有插件组件
plugin_path = "src.plugins" loaded_actions, loaded_commands = plugin_loader.load_all_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())
# 加载命令处理系统 # 加载命令处理系统
try: try:
@@ -210,6 +150,11 @@ class MainSystem:
logger.error(f"加载命令处理系统失败: {e}") logger.error(f"加载命令处理系统失败: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) 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): async def schedule_tasks(self):
"""调度定时任务""" """调度定时任务"""

View File

@@ -35,7 +35,7 @@ class PicAction(PluginAction):
"当有人要求你生成并发送一张图片时使用", "当有人要求你生成并发送一张图片时使用",
"当有人让你画一张图时使用", "当有人让你画一张图时使用",
] ]
enable_plugin = True enable_plugin = False
action_config_file_name = "pic_action_config.toml" action_config_file_name = "pic_action_config.toml"
# 激活类型设置 # 激活类型设置

View File

@@ -0,0 +1,105 @@
# 发送消息命令插件
这个插件提供了多个便捷的消息发送命令,允许管理员向指定群聊或用户发送消息。
## 命令列表
### 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,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+(?P<detail>full|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)}"

View File

@@ -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+(?P<target_type>group|user)\s+(?P<target_id>\d+)\s+(?P<content>.+)$"
command_help = "使用方法: /send <group|user> <ID> <消息内容> - 发送消息到指定群聊或用户"
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

View File

@@ -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+(?P<msg_type>text|image|emoji)\s+(?P<target_type>group|user)\s+(?P<target_id>\d+)(?:\s+(?P<platform>\w+))?\s+(?P<content>.+)$"
command_help = "使用方法: /sendfull <消息类型> <目标类型> <ID> [平台] <内容>"
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<group_id>\d+)\s+(?P<content>.+)$"
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<user_id>\d+)\s+(?P<content>.+)$"
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)}"

View File

@@ -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+(?P<target_type>group|user|here|reply)\s+(?P<target_id_or_content>.*?)(?:\s+(?P<content>.*))?$"
command_help = "使用方法: /csend <target_type> <参数> [内容]"
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)}"

View File

@@ -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<content>.+)$" # 匹配 /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)}"

View File

@@ -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()

View File

@@ -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. 查看日志中的详细错误信息