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

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