重构整个插件系统,尝试恢复可启动性,新增插件系统maibot-plugin-sdk依赖

This commit is contained in:
DrSmoothl
2026-03-07 19:40:51 +08:00
parent 2e3dd44ee9
commit ce8d8dfd0a
90 changed files with 3785 additions and 10061 deletions

View File

@@ -1,34 +1,38 @@
{
"manifest_version": 1,
"name": "Emoji插件 (Emoji Actions)",
"version": "1.0.0",
"version": "2.0.0",
"description": "可以发送和管理Emoji",
"author": {
"name": "SengokuCola",
"url": "https://github.com/MaiM-with-u"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.10.0"
"min_version": "1.0.0"
},
"homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot",
"keywords": ["emoji", "action", "built-in"],
"categories": ["Emoji"],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": true,
"plugin_type": "action_provider",
"components": [
{
"type": "action",
"name": "emoji",
"name": "emoji",
"description": "发送表情包辅助表达情绪"
}
]
}
},
"capabilities": [
"emoji.get_random",
"message.get_recent",
"message.build_readable",
"llm.generate",
"send.emoji",
"config.get"
]
}

View File

@@ -1,151 +0,0 @@
import random
from typing import Tuple
# 导入新插件系统
from src.plugin_system import BaseAction, ActionActivationType
# 导入依赖的系统组件
from src.common.logger import get_logger
# 导入API模块 - 标准Python包方式
from src.plugin_system.apis import emoji_api, llm_api, message_api
# NoReplyAction已集成到heartFC_chat.py中不再需要导入
from src.config.config import global_config
logger = get_logger("emoji")
class EmojiAction(BaseAction):
"""表情动作 - 发送表情包"""
activation_type = ActionActivationType.RANDOM
random_activation_probability = global_config.emoji.emoji_chance
parallel_action = True
# 动作基本信息
action_name = "emoji"
action_description = "发送表情包辅助表达情绪"
# 动作参数定义
action_parameters = {}
# 动作使用场景
action_require = [
"发送表情包辅助表达情绪",
"表达情绪时可以选择使用",
"不要连续发送,如果你已经发过[表情包],就不要选择此动作",
]
# 关联类型
associated_types = ["emoji"]
async def execute(self) -> Tuple[bool, str]:
# sourcery skip: assign-if-exp, introduce-default-else, swap-if-else-branches, use-named-expression
"""执行表情动作"""
try:
# 1. 获取发送表情的原因
# reason = self.action_data.get("reason", "表达当前情绪")
reason = self.action_reasoning
# 2. 随机获取20个表情包
sampled_emojis = await emoji_api.get_random(30)
if not sampled_emojis:
logger.warning(f"{self.log_prefix} 无法获取随机表情包")
return False, "无法获取随机表情包"
# 3. 准备情感数据
emotion_map = {}
for b64, desc, emo in sampled_emojis:
if emo not in emotion_map:
emotion_map[emo] = []
emotion_map[emo].append((b64, desc))
available_emotions = list(emotion_map.keys())
available_emotions_str = ""
for emotion in available_emotions:
available_emotions_str += f"{emotion}\n"
if not available_emotions:
logger.warning(f"{self.log_prefix} 获取到的表情包均无情感标签, 将随机发送")
emoji_base64, emoji_description, _ = random.choice(sampled_emojis)
else:
# 获取最近的5条消息内容用于判断
recent_messages = message_api.get_recent_messages(chat_id=self.chat_id, limit=5)
messages_text = ""
if recent_messages:
# 使用message_api构建可读的消息字符串
messages_text = message_api.build_readable_messages(
messages=recent_messages,
timestamp_mode="normal_no_YMD",
truncate=False,
show_actions=False,
)
# 4. 构建prompt让LLM选择情感
prompt = f"""你正在进行QQ聊天你需要根据聊天记录选出一个合适的情感标签。
请你根据以下原因和聊天记录进行选择
原因:{reason}
聊天记录:
{messages_text}
这里是可用的情感标签:
{available_emotions_str}
请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。
"""
if global_config.debug.show_prompt:
logger.info(f"{self.log_prefix} 生成的LLM Prompt: {prompt}")
else:
logger.debug(f"{self.log_prefix} 生成的LLM Prompt: {prompt}")
# 5. 调用LLM
models = llm_api.get_available_models()
chat_model_config = models.get("utils") # 使用字典访问方式
if not chat_model_config:
logger.error(f"{self.log_prefix} 未找到'utils'模型配置无法调用LLM")
return False, "未找到'utils'模型配置"
success, chosen_emotion, _, _ = await llm_api.generate_with_model(
prompt, model_config=chat_model_config, request_type="emoji.select"
)
if not success:
logger.error(f"{self.log_prefix} LLM调用失败: {chosen_emotion}")
return False, f"LLM调用失败: {chosen_emotion}"
chosen_emotion = chosen_emotion.strip().replace('"', "").replace("'", "")
logger.info(f"{self.log_prefix} LLM选择的情感: {chosen_emotion}")
# 6. 根据选择的情感匹配表情包
if chosen_emotion in emotion_map:
emoji_base64, emoji_description = random.choice(emotion_map[chosen_emotion])
logger.info(f"{self.log_prefix} 发送表情包[{chosen_emotion}],原因: {reason}")
else:
logger.warning(
f"{self.log_prefix} LLM选择的情感 '{chosen_emotion}' 不在可用列表中, 将随机选择一个表情包"
)
emoji_base64, emoji_description, _ = random.choice(sampled_emojis)
# 7. 发送表情包
success = await self.send_emoji(emoji_base64)
if success:
# 存储动作信息
await self.store_action_info(
action_build_into_prompt=True,
action_prompt_display=f"你发送了表情包,原因:{reason}",
action_done=True,
)
return True, f"成功发送表情包:[表情包:{chosen_emotion}]"
else:
error_msg = "发送表情包失败"
logger.error(f"{self.log_prefix} {error_msg}")
await self.send_text("执行表情包动作失败")
return False, error_msg
except Exception as e:
logger.error(f"{self.log_prefix} 表情动作执行失败: {e}", exc_info=True)
return False, f"表情发送失败: {str(e)}"

View File

@@ -1,66 +1,116 @@
"""
核心动作插件
"""Emoji 插件 — 新 SDK 版本
将系统核心动作reply、no_reply、emoji转换为新插件系统格式
这是系统的内置插件,提供基础的聊天交互功能
根据聊天上下文的情感,使用 LLM 选择并发送合适的表情包。
"""
from typing import List, Tuple, Type
import random
# 导入新插件系统
from src.plugin_system import BasePlugin, register_plugin, ComponentInfo
from src.plugin_system.base.config_types import ConfigField
# 导入依赖的系统组件
from src.common.logger import get_logger
from src.plugins.built_in.emoji_plugin.emoji import EmojiAction
logger = get_logger("core_actions")
from maibot_sdk import MaiBotPlugin, Action
from maibot_sdk.types import ActivationType
@register_plugin
class CoreActionsPlugin(BasePlugin):
"""核心动作插件
class EmojiPlugin(MaiBotPlugin):
"""表情包插件"""
系统内置插件,提供基础的聊天交互功能:
- Reply: 回复动作
- NoReply: 不回复动作
- Emoji: 表情动作
@Action(
"emoji",
description="发送表情包辅助表达情绪",
activation_type=ActivationType.RANDOM,
activation_probability=0.3,
parallel_action=True,
action_require=[
"发送表情包辅助表达情绪",
"表达情绪时可以选择使用",
"不要连续发送,如果你已经发过[表情包],就不要选择此动作",
],
associated_types=["emoji"],
)
async def handle_emoji(self, stream_id: str = "", reasoning: str = "", chat_id: str = "", **kwargs):
"""执行表情动作"""
reason = reasoning or "表达当前情绪"
注意插件基本信息优先从_manifest.json文件中读取
"""
# 1. 随机获取30个表情包
result = await self.ctx.emoji.get_random(30)
if not result or not result.get("success"):
return False, "无法获取随机表情包"
# 插件基本信息
plugin_name: str = "core_actions" # 内部标识符
enable_plugin: bool = True
dependencies: list[str] = [] # 插件依赖列表
python_dependencies: list[str] = [] # Python包依赖列表
config_file_name: str = "config.toml"
sampled_emojis = result.get("emojis", [])
if not sampled_emojis:
return False, "无法获取随机表情包"
# 配置节描述
config_section_descriptions = {
"plugin": "插件启用配置",
"components": "核心组件启用配置",
}
# 2. 按情感分组
emotion_map: dict[str, list] = {}
for emoji in sampled_emojis:
emo = emoji.get("emotion", "")
if emo not in emotion_map:
emotion_map[emo] = []
emotion_map[emo].append(emoji)
# 配置Schema定义
config_schema: dict = {
"plugin": {
"enabled": ConfigField(type=bool, default=True, description="是否启用插件"),
"config_version": ConfigField(type=str, default="0.6.0", description="配置文件版本"),
},
"components": {
"enable_emoji": ConfigField(type=bool, default=True, description="是否启用发送表情/图片动作"),
},
}
available_emotions = list(emotion_map.keys())
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表"""
if not available_emotions:
# 无情感标签,随机发送
chosen = random.choice(sampled_emojis)
await self.ctx.send.emoji(chosen["base64"], stream_id)
return True, "随机发送了表情包"
# --- 根据配置注册组件 ---
components = []
if self.get_config("components.enable_emoji", True):
components.append((EmojiAction.get_action_info(), EmojiAction))
# 3. 获取最近消息作为上下文
messages_text = ""
if chat_id:
recent_result = await self.ctx.message.get_recent(chat_id=chat_id, limit=5)
if recent_result and recent_result.get("success"):
readable_result = await self.ctx.call_capability(
"message.build_readable",
chat_id=chat_id,
start_time=0,
end_time=0,
limit=5,
timestamp_mode="normal_no_YMD",
truncate=False,
)
if readable_result and readable_result.get("success"):
messages_text = readable_result.get("text", "")
return components
# 4. 构建 prompt 让 LLM 选择情感
available_emotions_str = "\n".join(available_emotions)
prompt = f"""你正在进行QQ聊天你需要根据聊天记录选出一个合适的情感标签。
请你根据以下原因和聊天记录进行选择
原因:{reason}
聊天记录:
{messages_text}
这里是可用的情感标签:
{available_emotions_str}
请直接返回最匹配的那个情感标签,不要进行任何解释或添加其他多余的文字。
"""
# 5. 调用 LLM
llm_result = await self.ctx.llm.generate(prompt=prompt, model_name="utils")
if not llm_result or not llm_result.get("success"):
chosen = random.choice(sampled_emojis)
await self.ctx.send.emoji(chosen["base64"], stream_id)
return True, "LLM调用失败随机发送了表情包"
chosen_emotion = llm_result.get("response", "").strip().replace('"', "").replace("'", "")
# 6. 根据选择的情感匹配表情包
if chosen_emotion in emotion_map:
chosen = random.choice(emotion_map[chosen_emotion])
else:
chosen = random.choice(sampled_emojis)
# 7. 发送
send_result = await self.ctx.send.emoji(chosen["base64"], stream_id)
if send_result and send_result.get("success"):
return True, f"成功发送表情包:[表情包:{chosen_emotion}]"
return False, "发送表情包失败"
async def on_load(self):
# 从插件配置读取 emoji_chance 来覆盖默认概率
config_result = await self.ctx.config.get("emoji.emoji_chance")
if config_result and isinstance(config_result, dict) and config_result.get("success"):
pass # 配置已在宿主端管理
def create_plugin():
return EmojiPlugin()

View File

@@ -0,0 +1,33 @@
{
"manifest_version": 1,
"name": "LPMM 知识库插件 (Knowledge Search)",
"version": "2.0.0",
"description": "从 LPMM 知识库中搜索相关信息,供 LLM 工具调用",
"author": {
"name": "MaiBot团队",
"url": "https://github.com/MaiM-with-u"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "1.0.0"
},
"homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot",
"keywords": ["knowledge", "lpmm", "search", "tool", "built-in"],
"categories": ["Knowledge", "Tools"],
"default_locale": "zh-CN",
"plugin_info": {
"is_built_in": true,
"plugin_type": "tool_provider",
"components": [
{
"type": "tool",
"name": "lpmm_search_knowledge",
"description": "从知识库中搜索相关信息"
}
]
},
"capabilities": [
"knowledge.search"
]
}

View File

@@ -1,62 +0,0 @@
from typing import Dict, Any
from src.common.logger import get_logger
from src.config.config import global_config
from src.chat.knowledge import qa_manager
from src.plugin_system import BaseTool, ToolParamType
logger = get_logger("lpmm_get_knowledge_tool")
class SearchKnowledgeFromLPMMTool(BaseTool):
"""从LPMM知识库中搜索相关信息的工具"""
name = "lpmm_search_knowledge"
description = "从知识库中搜索相关信息,如果你需要知识,就使用这个工具"
parameters = [
("query", ToolParamType.STRING, "搜索查询关键词", True, None),
("limit", ToolParamType.INTEGER, "希望返回的相关知识条数默认5", False, None),
]
available_for_llm = global_config.lpmm_knowledge.enable
async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
"""执行知识库搜索
Args:
function_args: 工具参数
Returns:
Dict: 工具执行结果
"""
try:
query: str = function_args.get("query") # type: ignore
limit = function_args.get("limit", 5)
try:
limit_value = int(limit)
except (TypeError, ValueError):
limit_value = 5
limit_value = max(1, limit_value)
# threshold = function_args.get("threshold", 0.4)
# 检查LPMM知识库是否启用
if qa_manager is None:
logger.debug("LPMM知识库已禁用跳过知识获取")
return {"type": "info", "id": query, "content": "LPMM知识库已禁用"}
# 调用知识库搜索
knowledge_info = await qa_manager.get_knowledge(query, limit=limit_value)
logger.debug(f"知识库查询结果: {knowledge_info}")
if knowledge_info:
content = f"你知道这些知识: {knowledge_info}"
else:
content = f"你不太了解有关{query}的知识"
return {"type": "lpmm_knowledge", "id": query, "content": content}
except Exception as e:
# 捕获异常并记录错误
logger.error(f"知识库搜索工具执行失败: {str(e)}")
# 在其他异常情况下,确保 id 仍然是 query (如果它被定义了)
query_id = query if "query" in locals() else "unknown_query"
return {"type": "info", "id": query_id, "content": f"lpmm知识库搜索失败炸了: {str(e)}"}

View File

@@ -0,0 +1,39 @@
"""LPMM 知识库搜索插件 — 新 SDK 版本
提供 LLM 可调用的知识库搜索工具。
"""
from maibot_sdk import MaiBotPlugin, Tool
from maibot_sdk.types import ToolParameterInfo, ToolParamType
class KnowledgePlugin(MaiBotPlugin):
"""LPMM 知识库插件"""
@Tool(
"lpmm_search_knowledge",
description="从知识库中搜索相关信息,如果你需要知识,就使用这个工具",
parameters=[
ToolParameterInfo(name="query", param_type=ToolParamType.STRING, description="搜索查询关键词", required=True),
ToolParameterInfo(name="limit", param_type=ToolParamType.INTEGER, description="希望返回的相关知识条数默认5", required=False, default=5),
],
)
async def handle_lpmm_search_knowledge(self, query: str = "", limit: int = 5, **kwargs):
"""执行知识库搜索"""
if not query:
return {"type": "info", "id": "", "content": "未提供搜索关键词"}
try:
limit_value = max(1, int(limit))
except (TypeError, ValueError):
limit_value = 5
result = await self.ctx.call_capability("knowledge.search", query=query, limit=limit_value)
if result and result.get("success"):
content = result.get("content", f"你不太了解有关{query}的知识")
return {"type": "lpmm_knowledge", "id": query, "content": content}
return {"type": "info", "id": query, "content": f"知识库搜索失败: {result}"}
def create_plugin():
return KnowledgePlugin()

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"name": "插件和组件管理 (Plugin and Component Management)",
"version": "1.0.0",
"version": "2.0.0",
"description": "通过系统API管理插件和组件的生命周期包括加载、卸载、启用和禁用等操作。",
"author": {
"name": "MaiBot团队",
@@ -9,7 +9,7 @@
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.10.1"
"min_version": "1.0.0"
},
"homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot",
@@ -28,10 +28,22 @@
"plugin_info": {
"is_built_in": true,
"plugin_type": "plugin_management",
"capabilities": [
"component.get_all_plugins",
"component.list_loaded_plugins",
"component.list_registered_plugins",
"component.enable",
"component.disable",
"component.load_plugin",
"component.unload_plugin",
"component.reload_plugin",
"send.text",
"config.get"
],
"components": [
{
"type": "command",
"name": "plugin_management",
"name": "management",
"description": "管理插件和组件的生命周期,包括加载、卸载、启用和禁用等操作。"
}
]

View File

@@ -1,454 +1,279 @@
import asyncio
"""插件和组件管理 — 新 SDK 版本
from typing import List, Tuple, Type
from src.plugin_system import (
BasePlugin,
BaseCommand,
CommandInfo,
ConfigField,
register_plugin,
plugin_manage_api,
component_manage_api,
ComponentInfo,
ComponentType,
send_api,
通过 /pm 命令管理插件和组件的生命周期。
"""
from maibot_sdk import MaiBotPlugin, Command
_VALID_COMPONENT_TYPES = ("action", "command", "event_handler")
HELP_ALL = (
"管理命令帮助\n"
"/pm help 管理命令提示\n"
"/pm plugin 插件管理命令\n"
"/pm component 组件管理命令\n"
"使用 /pm plugin help 或 /pm component help 获取具体帮助"
)
HELP_PLUGIN = (
"插件管理命令帮助\n"
"/pm plugin help 插件管理命令提示\n"
"/pm plugin list 列出所有注册的插件\n"
"/pm plugin list_enabled 列出所有加载(启用)的插件\n"
"/pm plugin load <plugin_name> 加载指定插件\n"
"/pm plugin unload <plugin_name> 卸载指定插件\n"
"/pm plugin reload <plugin_name> 重新加载指定插件\n"
)
HELP_COMPONENT = (
"组件管理命令帮助\n"
"/pm component help 组件管理命令提示\n"
"/pm component list 列出所有注册的组件\n"
"/pm component list enabled <可选: type> 列出所有启用的组件\n"
"/pm component list disabled <可选: type> 列出所有禁用的组件\n"
" - <type> 可选项: local代表当前聊天中的global代表全局的\n"
" - <type> 不填时为 global\n"
"/pm component list type <component_type> 列出已经注册的指定类型的组件\n"
"/pm component enable global <component_name> <component_type> 全局启用组件\n"
"/pm component enable local <component_name> <component_type> 本聊天启用组件\n"
"/pm component disable global <component_name> <component_type> 全局禁用组件\n"
"/pm component disable local <component_name> <component_type> 本聊天禁用组件\n"
" - <component_type> 可选项: action, command, event_handler\n"
)
class ManagementCommand(BaseCommand):
command_name: str = "management"
description: str = "管理命令"
command_pattern: str = r"(?P<manage_command>^/pm(\s[a-zA-Z0-9_]+)*\s*$)"
class PluginManagementPlugin(MaiBotPlugin):
"""插件和组件管理插件"""
async def execute(self) -> Tuple[bool, str, bool]:
# sourcery skip: merge-duplicate-blocks
if (
not self.message
or not self.message.message_info
or not self.message.message_info.user_info
or str(self.message.message_info.user_info.user_id) not in self.get_config("plugin.permission", []) # type: ignore
):
await self._send_message("你没有权限使用插件管理命令")
@Command(
"management",
description="管理插件和组件的生命周期",
pattern=r"(?P<manage_command>^/pm(\s[a-zA-Z0-9_]+)*\s*$)",
)
async def handle_management(
self, stream_id: str = "", user_id: str = "", matched_groups: dict | None = None, **kwargs
):
"""处理 /pm 命令"""
# 权限检查
permission_result = await self.ctx.config.get("plugin.permission")
permission_list = permission_result if isinstance(permission_result, list) else []
if str(user_id) not in permission_list:
await self.ctx.send.text("你没有权限使用插件管理命令", stream_id)
return False, "没有权限", True
if not self.message.chat_stream:
await self._send_message("无法获取聊天流信息")
if not stream_id:
return False, "无法获取聊天流信息", True
self.stream_id = self.message.chat_stream.stream_id
if not self.stream_id:
await self._send_message("无法获取聊天流信息")
return False, "无法获取聊天流信息", True
command_list = self.matched_groups["manage_command"].strip().split(" ")
if len(command_list) == 1:
await self.show_help("all")
raw_command = (matched_groups or {}).get("manage_command", "").strip()
parts = raw_command.split(" ") if raw_command else ["/pm"]
n = len(parts)
# /pm
if n == 1:
await self.ctx.send.text(HELP_ALL, stream_id)
return True, "帮助已发送", True
if len(command_list) == 2:
match command_list[1]:
case "plugin":
await self.show_help("plugin")
case "component":
await self.show_help("component")
case "help":
await self.show_help("all")
case _:
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
if len(command_list) == 3:
if command_list[1] == "plugin":
match command_list[2]:
case "help":
await self.show_help("plugin")
case "list":
await self._list_registered_plugins()
case "list_enabled":
await self._list_loaded_plugins()
case "rescan":
await self._rescan_plugin_dirs()
case _:
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
elif command_list[1] == "component":
if command_list[2] == "list":
await self._list_all_registered_components()
elif command_list[2] == "help":
await self.show_help("component")
else:
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
else:
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
if len(command_list) == 4:
if command_list[1] == "plugin":
match command_list[2]:
case "load":
await self._load_plugin(command_list[3])
case "unload":
await self._unload_plugin(command_list[3])
case "reload":
await self._reload_plugin(command_list[3])
case "add_dir":
await self._add_dir(command_list[3])
case _:
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
elif command_list[1] == "component":
if command_list[2] != "list":
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
if command_list[3] == "enabled":
await self._list_enabled_components()
elif command_list[3] == "disabled":
await self._list_disabled_components()
else:
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
else:
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
if len(command_list) == 5:
if command_list[1] != "component":
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
if command_list[2] != "list":
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
if command_list[3] == "enabled":
await self._list_enabled_components(target_type=command_list[4])
elif command_list[3] == "disabled":
await self._list_disabled_components(target_type=command_list[4])
elif command_list[3] == "type":
await self._list_registered_components_by_type(command_list[4])
else:
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
if len(command_list) == 6:
if command_list[1] != "component":
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
if command_list[2] == "enable":
if command_list[3] == "global":
await self._globally_enable_component(command_list[4], command_list[5])
elif command_list[3] == "local":
await self._locally_enable_component(command_list[4], command_list[5])
else:
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
elif command_list[2] == "disable":
if command_list[3] == "global":
await self._globally_disable_component(command_list[4], command_list[5])
elif command_list[3] == "local":
await self._locally_disable_component(command_list[4], command_list[5])
else:
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
else:
await self._send_message("插件管理命令不合法")
return False, "命令不合法", True
return True, "命令执行完成", True
# /pm <sub>
if n == 2:
sub = parts[1]
if sub == "plugin":
await self.ctx.send.text(HELP_PLUGIN, stream_id)
elif sub == "component":
await self.ctx.send.text(HELP_COMPONENT, stream_id)
elif sub == "help":
await self.ctx.send.text(HELP_ALL, stream_id)
else:
await self.ctx.send.text("插件管理命令不合法", stream_id)
return False, "命令不合法", True
return True, "帮助已发送", True
async def show_help(self, target: str):
help_msg = ""
match target:
case "all":
help_msg = (
"管理命令帮助\n"
"/pm help 管理命令提示\n"
"/pm plugin 插件管理命令\n"
"/pm component 组件管理命令\n"
"使用 /pm plugin help 或 /pm component help 获取具体帮助"
)
case "plugin":
help_msg = (
"插件管理命令帮助\n"
"/pm plugin help 插件管理命令提示\n"
"/pm plugin list 列出所有注册的插件\n"
"/pm plugin list_enabled 列出所有加载(启用)的插件\n"
"/pm plugin rescan 重新扫描所有目录\n"
"/pm plugin load <plugin_name> 加载指定插件\n"
"/pm plugin unload <plugin_name> 卸载指定插件\n"
"/pm plugin reload <plugin_name> 重新加载指定插件\n"
"/pm plugin add_dir <directory_path> 添加插件目录\n"
)
case "component":
help_msg = (
"组件管理命令帮助\n"
"/pm component help 组件管理命令提示\n"
"/pm component list 列出所有注册的组件\n"
"/pm component list enabled <可选: type> 列出所有启用的组件\n"
"/pm component list disabled <可选: type> 列出所有禁用的组件\n"
" - <type> 可选项: local代表当前聊天中的global代表全局的\n"
" - <type> 不填时为 global\n"
"/pm component list type <component_type> 列出已经注册的指定类型的组件\n"
"/pm component enable global <component_name> <component_type> 全局启用组件\n"
"/pm component enable local <component_name> <component_type> 本聊天启用组件\n"
"/pm component disable global <component_name> <component_type> 全局禁用组件\n"
"/pm component disable local <component_name> <component_type> 本聊天禁用组件\n"
" - <component_type> 可选项: action, command, event_handler\n"
)
# /pm plugin <action> / /pm component <action>
if n == 3:
if parts[1] == "plugin":
await self._handle_plugin_3(parts[2], stream_id)
elif parts[1] == "component":
if parts[2] == "list":
await self._list_all_components(stream_id)
elif parts[2] == "help":
await self.ctx.send.text(HELP_COMPONENT, stream_id)
else:
await self.ctx.send.text("插件管理命令不合法", stream_id)
return False, "命令不合法", True
else:
await self.ctx.send.text("插件管理命令不合法", stream_id)
return False, "命令不合法", True
return True, "命令执行完成", True
if n == 4:
if parts[1] == "plugin":
await self._handle_plugin_4(parts[2], parts[3], stream_id)
elif parts[1] == "component":
if parts[2] == "list":
await self._handle_component_list_4(parts[3], stream_id)
else:
await self.ctx.send.text("插件管理命令不合法", stream_id)
return False, "命令不合法", True
else:
await self.ctx.send.text("插件管理命令不合法", stream_id)
return False, "命令不合法", True
return True, "命令执行完成", True
if n == 5:
if parts[1] != "component" or parts[2] != "list":
await self.ctx.send.text("插件管理命令不合法", stream_id)
return False, "命令不合法", True
await self._handle_component_list_5(parts[3], parts[4], stream_id)
return True, "命令执行完成", True
if n == 6:
if parts[1] != "component":
await self.ctx.send.text("插件管理命令不合法", stream_id)
return False, "命令不合法", True
await self._handle_component_toggle(parts[2], parts[3], parts[4], parts[5], stream_id)
return True, "命令执行完成", True
await self.ctx.send.text("插件管理命令不合法", stream_id)
return False, "命令不合法", True
# ------ plugin 子命令 ------
async def _handle_plugin_3(self, action: str, stream_id: str):
match action:
case "help":
await self.ctx.send.text(HELP_PLUGIN, stream_id)
case "list":
result = await self.ctx.component.list_registered_plugins()
plugins = result if isinstance(result, list) else []
await self.ctx.send.text(f"已注册的插件: {', '.join(plugins) if plugins else ''}", stream_id)
case "list_enabled":
result = await self.ctx.component.list_loaded_plugins()
plugins = result if isinstance(result, list) else []
await self.ctx.send.text(f"已加载的插件: {', '.join(plugins) if plugins else ''}", stream_id)
case _:
await self.ctx.send.text("插件管理命令不合法", stream_id)
async def _handle_plugin_4(self, action: str, name: str, stream_id: str):
match action:
case "load":
result = await self.ctx.component.load_plugin(name)
ok = result.get("success", False) if isinstance(result, dict) else bool(result)
msg = f"插件加载成功: {name}" if ok else f"插件加载失败: {name}"
await self.ctx.send.text(msg, stream_id)
case "unload":
result = await self.ctx.component.unload_plugin(name)
ok = result.get("success", False) if isinstance(result, dict) else bool(result)
msg = f"插件卸载成功: {name}" if ok else f"插件卸载失败: {name}"
await self.ctx.send.text(msg, stream_id)
case "reload":
result = await self.ctx.component.reload_plugin(name)
ok = result.get("success", False) if isinstance(result, dict) else bool(result)
msg = f"插件重新加载成功: {name}" if ok else f"插件重新加载失败: {name}"
await self.ctx.send.text(msg, stream_id)
case _:
await self.ctx.send.text("插件管理命令不合法", stream_id)
# ------ component 子命令 ------
async def _list_all_components(self, stream_id: str):
result = await self.ctx.component.get_all_plugins()
if not result:
await self.ctx.send.text("没有注册的组件", stream_id)
return
components = self._extract_components(result)
if not components:
await self.ctx.send.text("没有注册的组件", stream_id)
return
text = ", ".join(f"{c['name']} ({c['type']})" for c in components)
await self.ctx.send.text(f"已注册的组件: {text}", stream_id)
async def _handle_component_list_4(self, sub: str, stream_id: str):
if sub == "enabled":
await self._list_filtered_components("enabled", "global", stream_id)
elif sub == "disabled":
await self._list_filtered_components("disabled", "global", stream_id)
else:
await self.ctx.send.text("插件管理命令不合法", stream_id)
async def _handle_component_list_5(self, sub: str, arg: str, stream_id: str):
if sub in ("enabled", "disabled"):
await self._list_filtered_components(sub, arg, stream_id)
elif sub == "type":
if arg not in _VALID_COMPONENT_TYPES:
await self.ctx.send.text(f"未知组件类型: {arg}", stream_id)
return
await self._send_message(help_msg)
async def _list_loaded_plugins(self):
plugins = plugin_manage_api.list_loaded_plugins()
await self._send_message(f"已加载的插件: {', '.join(plugins)}")
async def _list_registered_plugins(self):
plugins = plugin_manage_api.list_registered_plugins()
await self._send_message(f"已注册的插件: {', '.join(plugins)}")
async def _rescan_plugin_dirs(self):
plugin_manage_api.rescan_plugin_directory()
await self._send_message("插件目录重新扫描执行中")
async def _load_plugin(self, plugin_name: str):
success, count = plugin_manage_api.load_plugin(plugin_name)
if success:
await self._send_message(f"插件加载成功: {plugin_name}")
result = await self.ctx.component.get_all_plugins()
components = [c for c in self._extract_components(result) if c.get("type") == arg]
if not components:
await self.ctx.send.text(f"没有注册的 {arg} 组件", stream_id)
return
text = ", ".join(f"{c['name']} ({c['type']})" for c in components)
await self.ctx.send.text(f"注册的 {arg} 组件: {text}", stream_id)
else:
if count == 0:
await self._send_message(f"插件{plugin_name}为禁用状态")
await self._send_message(f"插件加载失败: {plugin_name}")
await self.ctx.send.text("插件管理命令不合法", stream_id)
async def _unload_plugin(self, plugin_name: str):
success = await plugin_manage_api.remove_plugin(plugin_name)
if success:
await self._send_message(f"插件卸载成功: {plugin_name}")
async def _list_filtered_components(self, filter_mode: str, scope: str, stream_id: str):
result = await self.ctx.component.get_all_plugins()
all_components = self._extract_components(result)
if not all_components:
await self.ctx.send.text("没有注册的组件", stream_id)
return
if filter_mode == "enabled":
filtered = [c for c in all_components if c.get("enabled", False)]
label = "已启用"
else:
await self._send_message(f"插件卸载失败: {plugin_name}")
filtered = [c for c in all_components if not c.get("enabled", False)]
label = "已禁用"
async def _reload_plugin(self, plugin_name: str):
success = await plugin_manage_api.reload_plugin(plugin_name)
if success:
await self._send_message(f"插件重新加载成功: {plugin_name}")
scope_label = "全局" if scope == "global" else "本聊天"
if not filtered:
await self.ctx.send.text(f"没有满足条件的{label}{scope_label}组件", stream_id)
return
text = ", ".join(f"{c['name']} ({c['type']})" for c in filtered)
await self.ctx.send.text(f"满足条件的{label}{scope_label}组件: {text}", stream_id)
async def _handle_component_toggle(
self, action: str, scope: str, comp_name: str, comp_type: str, stream_id: str
):
if action not in ("enable", "disable"):
await self.ctx.send.text("插件管理命令不合法", stream_id)
return
if scope not in ("global", "local"):
await self.ctx.send.text("插件管理命令不合法", stream_id)
return
if comp_type not in _VALID_COMPONENT_TYPES:
await self.ctx.send.text(f"未知组件类型: {comp_type}", stream_id)
return
if action == "enable":
result = await self.ctx.component.enable_component(
comp_name, comp_type, scope=scope, stream_id=stream_id
)
else:
await self._send_message(f"插件重新加载失败: {plugin_name}")
result = await self.ctx.component.disable_component(
comp_name, comp_type, scope=scope, stream_id=stream_id
)
async def _add_dir(self, dir_path: str):
await self._send_message(f"正在添加插件目录: {dir_path}")
success = plugin_manage_api.add_plugin_directory(dir_path)
await asyncio.sleep(0.5) # 防止乱序发送
if success:
await self._send_message(f"插件目录添加成功: {dir_path}")
else:
await self._send_message(f"插件目录添加失败: {dir_path}")
ok = result.get("success", False) if isinstance(result, dict) else bool(result)
scope_label = "全局" if scope == "global" else "本地"
action_label = "启用" if action == "enable" else "禁用"
status = "成功" if ok else "失败"
await self.ctx.send.text(f"{scope_label}{action_label}组件{status}: {comp_name}", stream_id)
def _fetch_all_registered_components(self) -> List[ComponentInfo]:
all_plugin_info = component_manage_api.get_all_plugin_info()
if not all_plugin_info:
# ------ helpers ------
@staticmethod
def _extract_components(result) -> list[dict]:
"""从 get_all_plugins 结果中提取所有组件列表"""
if not result:
return []
components_info: List[ComponentInfo] = []
for plugin_info in all_plugin_info.values():
components_info.extend(plugin_info.components)
return components_info
def _fetch_locally_disabled_components(self) -> List[str]:
locally_disabled_components_actions = component_manage_api.get_locally_disabled_components(
self.message.chat_stream.stream_id, ComponentType.ACTION
)
locally_disabled_components_commands = component_manage_api.get_locally_disabled_components(
self.message.chat_stream.stream_id, ComponentType.COMMAND
)
locally_disabled_components_event_handlers = component_manage_api.get_locally_disabled_components(
self.message.chat_stream.stream_id, ComponentType.EVENT_HANDLER
)
return (
locally_disabled_components_actions
+ locally_disabled_components_commands
+ locally_disabled_components_event_handlers
)
async def _list_all_registered_components(self):
components_info = self._fetch_all_registered_components()
if not components_info:
await self._send_message("没有注册的组件")
return
all_components_str = ", ".join(
f"{component.name} ({component.component_type})" for component in components_info
)
await self._send_message(f"已注册的组件: {all_components_str}")
async def _list_enabled_components(self, target_type: str = "global"):
components_info = self._fetch_all_registered_components()
if not components_info:
await self._send_message("没有注册的组件")
return
if target_type == "global":
enabled_components = [component for component in components_info if component.enabled]
if not enabled_components:
await self._send_message("没有满足条件的已启用全局组件")
return
enabled_components_str = ", ".join(
f"{component.name} ({component.component_type})" for component in enabled_components
)
await self._send_message(f"满足条件的已启用全局组件: {enabled_components_str}")
elif target_type == "local":
locally_disabled_components = self._fetch_locally_disabled_components()
enabled_components = [
component
for component in components_info
if (component.name not in locally_disabled_components and component.enabled)
]
if not enabled_components:
await self._send_message("本聊天没有满足条件的已启用组件")
return
enabled_components_str = ", ".join(
f"{component.name} ({component.component_type})" for component in enabled_components
)
await self._send_message(f"本聊天满足条件的已启用组件: {enabled_components_str}")
async def _list_disabled_components(self, target_type: str = "global"):
components_info = self._fetch_all_registered_components()
if not components_info:
await self._send_message("没有注册的组件")
return
if target_type == "global":
disabled_components = [component for component in components_info if not component.enabled]
if not disabled_components:
await self._send_message("没有满足条件的已禁用全局组件")
return
disabled_components_str = ", ".join(
f"{component.name} ({component.component_type})" for component in disabled_components
)
await self._send_message(f"满足条件的已禁用全局组件: {disabled_components_str}")
elif target_type == "local":
locally_disabled_components = self._fetch_locally_disabled_components()
disabled_components = [
component
for component in components_info
if (component.name in locally_disabled_components or not component.enabled)
]
if not disabled_components:
await self._send_message("本聊天没有满足条件的已禁用组件")
return
disabled_components_str = ", ".join(
f"{component.name} ({component.component_type})" for component in disabled_components
)
await self._send_message(f"本聊天满足条件的已禁用组件: {disabled_components_str}")
async def _list_registered_components_by_type(self, target_type: str):
match target_type:
case "action":
component_type = ComponentType.ACTION
case "command":
component_type = ComponentType.COMMAND
case "event_handler":
component_type = ComponentType.EVENT_HANDLER
case _:
await self._send_message(f"未知组件类型: {target_type}")
return
components_info = component_manage_api.get_components_info_by_type(component_type)
if not components_info:
await self._send_message(f"没有注册的 {target_type} 组件")
return
components_str = ", ".join(
f"{name} ({component.component_type})" for name, component in components_info.items()
)
await self._send_message(f"注册的 {target_type} 组件: {components_str}")
async def _globally_enable_component(self, component_name: str, component_type: str):
match component_type:
case "action":
target_component_type = ComponentType.ACTION
case "command":
target_component_type = ComponentType.COMMAND
case "event_handler":
target_component_type = ComponentType.EVENT_HANDLER
case _:
await self._send_message(f"未知组件类型: {component_type}")
return
if component_manage_api.globally_enable_component(component_name, target_component_type):
await self._send_message(f"全局启用组件成功: {component_name}")
else:
await self._send_message(f"全局启用组件失败: {component_name}")
async def _globally_disable_component(self, component_name: str, component_type: str):
match component_type:
case "action":
target_component_type = ComponentType.ACTION
case "command":
target_component_type = ComponentType.COMMAND
case "event_handler":
target_component_type = ComponentType.EVENT_HANDLER
case _:
await self._send_message(f"未知组件类型: {component_type}")
return
success = await component_manage_api.globally_disable_component(component_name, target_component_type)
if success:
await self._send_message(f"全局禁用组件成功: {component_name}")
else:
await self._send_message(f"全局禁用组件失败: {component_name}")
async def _locally_enable_component(self, component_name: str, component_type: str):
match component_type:
case "action":
target_component_type = ComponentType.ACTION
case "command":
target_component_type = ComponentType.COMMAND
case "event_handler":
target_component_type = ComponentType.EVENT_HANDLER
case _:
await self._send_message(f"未知组件类型: {component_type}")
return
if component_manage_api.locally_enable_component(
component_name,
target_component_type,
self.message.chat_stream.stream_id,
):
await self._send_message(f"本地启用组件成功: {component_name}")
else:
await self._send_message(f"本地启用组件失败: {component_name}")
async def _locally_disable_component(self, component_name: str, component_type: str):
match component_type:
case "action":
target_component_type = ComponentType.ACTION
case "command":
target_component_type = ComponentType.COMMAND
case "event_handler":
target_component_type = ComponentType.EVENT_HANDLER
case _:
await self._send_message(f"未知组件类型: {component_type}")
return
if component_manage_api.locally_disable_component(
component_name,
target_component_type,
self.message.chat_stream.stream_id,
):
await self._send_message(f"本地禁用组件成功: {component_name}")
else:
await self._send_message(f"本地禁用组件失败: {component_name}")
async def _send_message(self, message: str):
await send_api.text_to_stream(message, self.stream_id, typing=False, storage_message=False)
if isinstance(result, dict):
components = []
for plugin_info in result.values():
if isinstance(plugin_info, dict):
components.extend(plugin_info.get("components", []))
return components
return []
@register_plugin
class PluginManagementPlugin(BasePlugin):
plugin_name: str = "plugin_management_plugin"
enable_plugin: bool = False
dependencies: list[str] = []
python_dependencies: list[str] = []
config_file_name: str = "config.toml"
config_schema: dict = {
"plugin": {
"enabled": ConfigField(bool, default=False, description="是否启用插件"),
"config_version": ConfigField(type=str, default="1.1.0", description="配置文件版本"),
"permission": ConfigField(
list, default=[], description="有权限使用插件管理命令的用户列表请填写字符串形式的用户ID"
),
},
}
def get_plugin_components(self) -> List[Tuple[CommandInfo, Type[BaseCommand]]]:
components = []
if self.get_config("plugin.enabled", True):
components.append((ManagementCommand.get_command_info(), ManagementCommand))
return components
def create_plugin():
return PluginManagementPlugin()

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"name": "文本转语音插件 (Text-to-Speech)",
"version": "0.1.0",
"version": "2.0.0",
"description": "将文本转换为语音进行播放的插件,支持多种语音模式和智能语音输出场景判断。",
"author": {
"name": "MaiBot团队",
@@ -10,7 +10,7 @@
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.8.0"
"min_version": "1.0.0"
},
"homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot",

View File

@@ -1,145 +1,55 @@
from src.plugin_system.apis.plugin_register_api import register_plugin
from src.plugin_system.base.base_plugin import BasePlugin
from src.plugin_system.base.component_types import ComponentInfo
from src.common.logger import get_logger
from src.plugin_system.base.base_action import BaseAction, ActionActivationType
from src.plugin_system.base.config_types import ConfigField
from typing import Tuple, List, Type
"""TTS 插件 — 新 SDK 版本
logger = get_logger("tts")
将文本转换为语音进行播放。
"""
import re
from maibot_sdk import MaiBotPlugin, Action
from maibot_sdk.types import ActivationType
class TTSAction(BaseAction):
"""TTS语音转换动作处理类"""
# 激活设置
activation_type = ActionActivationType.KEYWORD
activation_keywords = ["语音", "tts", "播报", "读出来", "语音播放", "", "朗读"]
keyword_case_sensitive = False
parallel_action = False
# 动作基本信息
action_name = "tts_action"
action_description = "将文本转换为语音进行播放,适用于需要语音输出的场景"
# 动作参数定义
action_parameters = {
"voice_text": "你想用语音表达的内容,这段内容将会以语音形式发出",
}
# 动作使用场景
action_require = [
"当需要发送语音信息时使用",
"当用户明确要求使用语音功能时使用",
"当表达内容更适合用语音而不是文字传达时使用",
"当用户想听到语音回答而非阅读文本时使用",
]
# 关联类型
associated_types = ["tts_text"]
async def execute(self) -> Tuple[bool, str]:
"""处理TTS文本转语音动作"""
logger.info(f"{self.log_prefix} 执行TTS动作: {self.reasoning}")
# 获取要转换的文本
text = self.action_data.get("voice_text")
class TTSPlugin(MaiBotPlugin):
"""文本转语音插件"""
@Action(
"tts_action",
description="将文本转换为语音进行播放,适用于需要语音输出的场景",
activation_type=ActivationType.KEYWORD,
activation_keywords=["语音", "tts", "播报", "读出来", "语音播放", "", "朗读"],
parallel_action=False,
action_parameters={"voice_text": "你想用语音表达的内容,这段内容将会以语音形式发出"},
action_require=[
"当需要发送语音信息时使用",
"当用户明确要求使用语音功能时使用",
"当表达内容更适合用语音而不是文字传达时使用",
"当用户想听到语音回答而非阅读文本时使用",
],
associated_types=["tts_text"],
)
async def handle_tts_action(self, stream_id: str = "", action_data: dict = None, reasoning: str = "", **kwargs):
"""处理 TTS 文本转语音动作"""
action_data = action_data or {}
text = action_data.get("voice_text", "")
if not text:
logger.error(f"{self.log_prefix} 执行TTS动作时未提供文本内容")
return False, "执行TTS动作失败未提供文本内容"
# 确保文本适合TTS使用
processed_text = self._process_text_for_tts(text)
try:
# 发送TTS消息
await self.send_custom(message_type="tts_text", content=processed_text)
# 记录动作信息
await self.store_action_info(
action_build_into_prompt=True, action_prompt_display="已经发送了语音消息。", action_done=True
)
logger.info(f"{self.log_prefix} TTS动作执行成功文本长度: {len(processed_text)}")
return True, "TTS动作执行成功"
except Exception as e:
logger.error(f"{self.log_prefix} 执行TTS动作时出错: {e}")
return False, f"执行TTS动作时出错: {e}"
def _process_text_for_tts(self, text: str) -> str:
"""
处理文本使其更适合TTS使用
- 移除不必要的特殊字符和表情符号
- 修正标点符号以提高语音质量
- 优化文本结构使语音更流畅
"""
# 这里可以添加文本处理逻辑
# 例如:移除多余的标点、表情符号,优化语句结构等
# 简单示例实现
processed_text = text
# 移除多余的标点符号
import re
processed_text = re.sub(r"([!?,.;:。!?,、;:])\1+", r"\1", processed_text)
# 确保句子结尾有合适的标点
# 文本预处理
processed_text = re.sub(r"([!?,.;:。!?,、;:])\1+", r"\1", text)
if not any(processed_text.endswith(end) for end in [".", "?", "!", "", "", ""]):
processed_text = f"{processed_text}"
return processed_text
# 发送自定义 tts 消息
result = await self.ctx.call_capability(
"send.custom",
message_type="tts_text",
content=processed_text,
stream_id=stream_id,
)
if result and result.get("success"):
return True, "TTS动作执行成功"
return False, f"TTS动作执行失败: {result}"
@register_plugin
class TTSPlugin(BasePlugin):
"""TTS插件
- 这是文字转语音插件
- Normal模式下依靠关键词触发
- Focus模式下由LLM判断触发
- 具有一定的文本预处理能力
"""
# 插件基本信息
plugin_name: str = "tts_plugin" # 内部标识符
enable_plugin: bool = True
dependencies: list[str] = [] # 插件依赖列表
python_dependencies: list[str] = [] # Python包依赖列表
config_file_name: str = "config.toml"
# 配置节描述
config_section_descriptions = {
"plugin": "插件基本信息配置",
"components": "组件启用控制",
"logging": "日志记录相关配置",
}
# 配置Schema定义
config_schema: dict = {
"plugin": {
"name": ConfigField(type=str, default="tts_plugin", description="插件名称", required=True),
"version": ConfigField(type=str, default="0.1.0", description="插件版本号"),
"enabled": ConfigField(type=bool, default=True, description="是否启用插件"),
"description": ConfigField(type=str, default="文字转语音插件", description="插件描述", required=True),
},
"components": {"enable_tts": ConfigField(type=bool, default=True, description="是否启用TTS Action")},
"logging": {
"level": ConfigField(
type=str, default="INFO", description="日志记录级别", choices=["DEBUG", "INFO", "WARNING", "ERROR"]
),
"prefix": ConfigField(type=str, default="[TTS]", description="日志记录前缀"),
},
}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表"""
# 从配置获取组件启用状态
enable_tts = self.get_config("components.enable_tts", True)
components = [] # 添加Action组件
if enable_tts:
components.append((TTSAction.get_action_info(), TTSAction))
return components
def create_plugin():
return TTSPlugin()