重构整个插件系统,尝试恢复可启动性,新增插件系统maibot-plugin-sdk依赖
This commit is contained in:
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)}"
|
||||
@@ -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()
|
||||
|
||||
33
src/plugins/built_in/knowledge/_manifest.json
Normal file
33
src/plugins/built_in/knowledge/_manifest.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -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)}"}
|
||||
39
src/plugins/built_in/knowledge/plugin.py
Normal file
39
src/plugins/built_in/knowledge/plugin.py
Normal 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()
|
||||
@@ -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": "管理插件和组件的生命周期,包括加载、卸载、启用和禁用等操作。"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user