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
# 忽略 /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/*

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.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接口给插件开发者

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.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',
]

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 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:
"""发送消息的简化方法

View File

@@ -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]:
"""处理消息中的命令

View File

@@ -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):
"""调度定时任务"""

View File

@@ -35,7 +35,7 @@ class PicAction(PluginAction):
"当有人要求你生成并发送一张图片时使用",
"当有人让你画一张图时使用",
]
enable_plugin = True
enable_plugin = False
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. 查看日志中的详细错误信息