feat:将旧版示例插件更新,更新mute插件(tts,vtb,doubaopic持续炸裂中)

This commit is contained in:
SengokuCola
2025-06-10 23:36:45 +08:00
parent 9f90b2568c
commit 6455dab5b8
45 changed files with 1657 additions and 2001 deletions

View File

@@ -10,6 +10,7 @@ from typing import List, Tuple, Type, Optional
# 导入新插件系统
from src.plugin_system import BasePlugin, register_plugin, BaseAction, ComponentInfo, ActionActivationType, ChatMode
from src.plugin_system.base.base_command import BaseCommand
# 导入依赖的系统组件
from src.common.logger_manager import get_logger
@@ -341,19 +342,97 @@ class CoreActionsPlugin(BasePlugin):
return [
# 回复动作
(ReplyAction.get_action_info(name="reply", description="参与聊天回复,处理文本和表情的发送"), ReplyAction),
(ReplyAction.get_action_info(
name="reply",
description="参与聊天回复,处理文本和表情的发送"
), ReplyAction),
# 不回复动作
(
NoReplyAction.get_action_info(name="no_reply", description="暂时不回复消息,等待新消息或超时"),
NoReplyAction,
),
(NoReplyAction.get_action_info(
name="no_reply",
description="暂时不回复消息,等待新消息或超时"
), NoReplyAction),
# 表情动作
(EmojiAction.get_action_info(name="emoji", description="发送表情包辅助表达情绪"), EmojiAction),
(EmojiAction.get_action_info(
name="emoji",
description="发送表情包辅助表达情绪"
), EmojiAction),
# 退出专注聊天动作
(
ExitFocusChatAction.get_action_info(
name="exit_focus_chat", description="退出专注聊天,从专注模式切换到普通模式"
),
ExitFocusChatAction,
),
(ExitFocusChatAction.get_action_info(
name="exit_focus_chat",
description="退出专注聊天,从专注模式切换到普通模式"
), ExitFocusChatAction),
# 示例Command - Ping命令
(PingCommand.get_command_info(
name="ping",
description="测试机器人响应,拦截后续处理"
), PingCommand),
# 示例Command - Log命令
(LogCommand.get_command_info(
name="log",
description="记录消息到日志,不拦截后续处理"
), LogCommand)
]
# ===== 示例Command组件 =====
class PingCommand(BaseCommand):
"""Ping命令 - 测试响应,拦截消息处理"""
command_pattern = r"^/ping(\s+(?P<message>.+))?$"
command_help = "测试机器人响应 - 拦截后续处理"
command_examples = ["/ping", "/ping 测试消息"]
intercept_message = True # 拦截消息,不继续处理
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行ping命令"""
try:
message = self.matched_groups.get("message", "")
reply_text = f"🏓 Pong! {message}" if message else "🏓 Pong!"
await self.send_reply(reply_text)
return True, f"发送ping响应: {reply_text}"
except Exception as e:
logger.error(f"Ping命令执行失败: {e}")
return False, f"执行失败: {str(e)}"
class LogCommand(BaseCommand):
"""日志命令 - 记录消息但不拦截后续处理"""
command_pattern = r"^/log(\s+(?P<level>debug|info|warn|error))?$"
command_help = "记录当前消息到日志 - 不拦截后续处理"
command_examples = ["/log", "/log info", "/log debug"]
intercept_message = False # 不拦截消息,继续后续处理
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行日志命令"""
try:
level = self.matched_groups.get("level", "info")
user_nickname = self.message.message_info.user_info.user_nickname
content = self.message.processed_plain_text
log_message = f"[{level.upper()}] 用户 {user_nickname}: {content}"
# 根据级别记录日志
if level == "debug":
logger.debug(log_message)
elif level == "warn":
logger.warning(log_message)
elif level == "error":
logger.error(log_message)
else:
logger.info(log_message)
# 不发送回复,让消息继续处理
return True, f"已记录到{level}级别日志"
except Exception as e:
logger.error(f"Log命令执行失败: {e}")
return False, f"执行失败: {str(e)}"

View File

@@ -0,0 +1,32 @@
"""测试插件包:图片发送"""
"""
这是一个测试插件,用于测试图片发送功能
"""
"""豆包图片生成插件
这是一个基于火山引擎豆包模型的AI图片生成插件。
功能特性:
- 智能LLM判定根据聊天内容智能判断是否需要生成图片
- 高质量图片生成使用豆包Seed Dream模型生成图片
- 结果缓存:避免重复生成相同内容的图片
- 配置验证:自动验证和修复配置文件
- 参数验证:完整的输入参数验证和错误处理
- 多尺寸支持:支持多种图片尺寸生成
使用场景:
- 用户要求画图或生成图片时自动触发
- 将文字描述转换为视觉图像
- 创意图片和艺术作品生成
配置文件src/plugins/doubao_pic/actions/pic_action_config.toml
配置要求:
1. 设置火山引擎API密钥 (volcano_generate_api_key)
2. 配置API基础URL (base_url)
3. 选择合适的生成模型和参数
注意需要有效的火山引擎API访问权限才能正常使用。
"""

View File

@@ -0,0 +1,4 @@
"""测试插件动作模块"""
# 导入所有动作模块以确保装饰器被执行
from . import pic_action # noqa

View File

@@ -0,0 +1,122 @@
import os
import toml
from src.common.logger_manager import get_logger
logger = get_logger("pic_config")
CONFIG_CONTENT = """\
# 火山方舟 API 的基础 URL
base_url = "https://ark.cn-beijing.volces.com/api/v3"
# 用于图片生成的API密钥
volcano_generate_api_key = "YOUR_VOLCANO_GENERATE_API_KEY_HERE"
# 默认图片生成模型
default_model = "doubao-seedream-3-0-t2i-250415"
# 默认图片尺寸
default_size = "1024x1024"
# 是否默认开启水印
default_watermark = true
# 默认引导强度
default_guidance_scale = 2.5
# 默认随机种子
default_seed = 42
# 缓存设置
cache_enabled = true
cache_max_size = 10
# 更多插件特定配置可以在此添加...
# custom_parameter = "some_value"
"""
# 默认配置字典,用于验证和修复
DEFAULT_CONFIG = {
"base_url": "https://ark.cn-beijing.volces.com/api/v3",
"volcano_generate_api_key": "YOUR_VOLCANO_GENERATE_API_KEY_HERE",
"default_model": "doubao-seedream-3-0-t2i-250415",
"default_size": "1024x1024",
"default_watermark": True,
"default_guidance_scale": 2.5,
"default_seed": 42,
"cache_enabled": True,
"cache_max_size": 10,
}
def validate_and_fix_config(config_path: str) -> bool:
"""验证并修复配置文件"""
try:
with open(config_path, "r", encoding="utf-8") as f:
config = toml.load(f)
# 检查缺失的配置项
missing_keys = []
fixed = False
for key, default_value in DEFAULT_CONFIG.items():
if key not in config:
missing_keys.append(key)
config[key] = default_value
fixed = True
logger.info(f"添加缺失的配置项: {key} = {default_value}")
# 验证配置值的类型和范围
if isinstance(config.get("default_guidance_scale"), (int, float)):
if not 0.1 <= config["default_guidance_scale"] <= 20.0:
config["default_guidance_scale"] = 2.5
fixed = True
logger.info("修复无效的 default_guidance_scale 值")
if isinstance(config.get("default_seed"), (int, float)):
config["default_seed"] = int(config["default_seed"])
else:
config["default_seed"] = 42
fixed = True
logger.info("修复无效的 default_seed 值")
if config.get("cache_max_size") and not isinstance(config["cache_max_size"], int):
config["cache_max_size"] = 10
fixed = True
logger.info("修复无效的 cache_max_size 值")
# 如果有修复,写回文件
if fixed:
# 创建备份
backup_path = config_path + ".backup"
if os.path.exists(config_path):
os.rename(config_path, backup_path)
logger.info(f"已创建配置备份: {backup_path}")
# 写入修复后的配置
with open(config_path, "w", encoding="utf-8") as f:
toml.dump(config, f)
logger.info(f"配置文件已修复: {config_path}")
return True
except Exception as e:
logger.error(f"验证配置文件时出错: {e}")
return False
def generate_config():
# 获取当前脚本所在的目录
current_dir = os.path.dirname(os.path.abspath(__file__))
config_file_path = os.path.join(current_dir, "pic_action_config.toml")
if not os.path.exists(config_file_path):
try:
with open(config_file_path, "w", encoding="utf-8") as f:
f.write(CONFIG_CONTENT)
logger.info(f"配置文件已生成: {config_file_path}")
logger.info("请记得编辑该文件填入您的火山引擎API 密钥。")
except IOError as e:
logger.error(f"错误:无法写入配置文件 {config_file_path}。原因: {e}")
else:
# 验证并修复现有配置
validate_and_fix_config(config_file_path)
if __name__ == "__main__":
generate_config()

View File

@@ -0,0 +1,376 @@
import asyncio
import json
import urllib.request
import urllib.error
import base64 # 新增用于Base64编码
import traceback # 新增:用于打印堆栈跟踪
from typing import Tuple
from src.chat.actions.plugin_action import PluginAction, register_action
from src.chat.actions.base_action import ActionActivationType, ChatMode
from src.common.logger_manager import get_logger
from .generate_pic_config import generate_config
logger = get_logger("pic_action")
# 当此模块被加载时,尝试生成配置文件(如果它不存在)
# 注意:在某些插件加载机制下,这可能会在每次机器人启动或插件重载时执行
# 考虑是否需要更复杂的逻辑来决定何时运行 (例如,仅在首次安装时)
generate_config()
@register_action
class PicAction(PluginAction):
"""根据描述使用火山引擎HTTP API生成图片的动作处理类"""
action_name = "pic_action"
action_description = (
"可以根据特定的描述,生成并发送一张图片,如果没提供描述,就根据聊天内容生成,你可以立刻画好,不用等待"
)
action_parameters = {
"description": "图片描述,输入你想要生成并发送的图片的描述,必填",
"size": "图片尺寸,例如 '1024x1024' (可选, 默认从配置或 '1024x1024')",
}
action_require = [
"当有人让你画东西时使用,你可以立刻画好,不用等待",
"当有人要求你生成并发送一张图片时使用",
"当有人让你画一张图时使用",
]
enable_plugin = False
action_config_file_name = "pic_action_config.toml"
# 激活类型设置
focus_activation_type = ActionActivationType.LLM_JUDGE # Focus模式使用LLM判定精确理解需求
normal_activation_type = ActionActivationType.KEYWORD # Normal模式使用关键词激活快速响应
# 关键词设置用于Normal模式
activation_keywords = ["", "绘制", "生成图片", "画图", "draw", "paint", "图片生成"]
keyword_case_sensitive = False
# LLM判定提示词用于Focus模式
llm_judge_prompt = """
判定是否需要使用图片生成动作的条件:
1. 用户明确要求画图、生成图片或创作图像
2. 用户描述了想要看到的画面或场景
3. 对话中提到需要视觉化展示某些概念
4. 用户想要创意图片或艺术作品
适合使用的情况:
- "画一张...""画个...""生成图片"
- "我想看看...的样子"
- "能画出...吗"
- "创作一幅..."
绝对不要使用的情况:
1. 纯文字聊天和问答
2. 只是提到"图片"""等词但不是要求生成
3. 谈论已存在的图片或照片
4. 技术讨论中提到绘图概念但无生成需求
5. 用户明确表示不需要图片时
"""
# Random激活概率备用
random_activation_probability = 0.15 # 适中概率,图片生成比较有趣
# 简单的请求缓存,避免短时间内重复请求
_request_cache = {}
_cache_max_size = 10
# 模式启用设置 - 图片生成在所有模式下可用
mode_enable = ChatMode.ALL
# 并行执行设置 - 图片生成可以与回复并行执行,不覆盖回复内容
parallel_action = False
@classmethod
def _get_cache_key(cls, description: str, model: str, size: str) -> str:
"""生成缓存键"""
return f"{description[:100]}|{model}|{size}" # 限制描述长度避免键过长
@classmethod
def _cleanup_cache(cls):
"""清理缓存,保持大小在限制内"""
if len(cls._request_cache) > cls._cache_max_size:
# 简单的FIFO策略移除最旧的条目
keys_to_remove = list(cls._request_cache.keys())[: -cls._cache_max_size // 2]
for key in keys_to_remove:
del cls._request_cache[key]
def __init__(
self,
action_data: dict,
reasoning: str,
cycle_timers: dict,
thinking_id: str,
global_config: dict = None,
**kwargs,
):
super().__init__(action_data, reasoning, cycle_timers, thinking_id, global_config, **kwargs)
logger.info(f"{self.log_prefix} 开始绘图!原因是:{self.reasoning}")
http_base_url = self.config.get("base_url")
http_api_key = self.config.get("volcano_generate_api_key")
if not (http_base_url and http_api_key):
logger.error(
f"{self.log_prefix} PicAction初始化, 但HTTP配置 (base_url 或 volcano_generate_api_key) 缺失. HTTP图片生成将失败."
)
else:
logger.info(f"{self.log_prefix} HTTP方式初始化完成. Base URL: {http_base_url}, API Key已配置.")
# _restore_env_vars 方法不再需要,已移除
async def process(self) -> Tuple[bool, str]:
"""处理图片生成动作通过HTTP API"""
logger.info(f"{self.log_prefix} 执行 pic_action (HTTP): {self.reasoning}")
# 配置验证
http_base_url = self.config.get("base_url")
http_api_key = self.config.get("volcano_generate_api_key")
if not (http_base_url and http_api_key):
error_msg = "抱歉图片生成功能所需的HTTP配置如API地址或密钥不完整无法提供服务。"
await self.send_message_by_expressor(error_msg)
logger.error(f"{self.log_prefix} HTTP调用配置缺失: base_url 或 volcano_generate_api_key.")
return False, "HTTP配置不完整"
# API密钥验证
if http_api_key == "YOUR_VOLCANO_GENERATE_API_KEY_HERE":
error_msg = "图片生成功能尚未配置请设置正确的API密钥。"
await self.send_message_by_expressor(error_msg)
logger.error(f"{self.log_prefix} API密钥未配置")
return False, "API密钥未配置"
# 参数验证
description = self.action_data.get("description")
if not description or not description.strip():
logger.warning(f"{self.log_prefix} 图片描述为空,无法生成图片。")
await self.send_message_by_expressor("你需要告诉我想要画什么样的图片哦~ 比如说'画一只可爱的小猫'")
return False, "图片描述为空"
# 清理和验证描述
description = description.strip()
if len(description) > 1000: # 限制描述长度
description = description[:1000]
logger.info(f"{self.log_prefix} 图片描述过长,已截断")
# 获取配置
default_model = self.config.get("default_model", "doubao-seedream-3-0-t2i-250415")
image_size = self.action_data.get("size", self.config.get("default_size", "1024x1024"))
# 验证图片尺寸格式
if not self._validate_image_size(image_size):
logger.warning(f"{self.log_prefix} 无效的图片尺寸: {image_size},使用默认值")
image_size = "1024x1024"
# 检查缓存
cache_key = self._get_cache_key(description, default_model, image_size)
if cache_key in self._request_cache:
cached_result = self._request_cache[cache_key]
logger.info(f"{self.log_prefix} 使用缓存的图片结果")
await self.send_message_by_expressor("我之前画过类似的图片,用之前的结果~")
# 直接发送缓存的结果
send_success = await self.send_message(type="image", data=cached_result)
if send_success:
await self.send_message_by_expressor("图片表情已发送!")
return True, "图片表情已发送(缓存)"
else:
# 缓存失败,清除这个缓存项并继续正常流程
del self._request_cache[cache_key]
# guidance_scale 现在完全由配置文件控制
guidance_scale_input = self.config.get("default_guidance_scale", 2.5) # 默认2.5
guidance_scale_val = 2.5 # Fallback default
try:
guidance_scale_val = float(guidance_scale_input)
except (ValueError, TypeError):
logger.warning(
f"{self.log_prefix} 配置文件中的 default_guidance_scale 值 '{guidance_scale_input}' 无效 (应为浮点数),使用默认值 2.5。"
)
guidance_scale_val = 2.5
# Seed parameter - ensure it's always an integer
seed_config_value = self.config.get("default_seed")
seed_val = 42 # Default seed if not configured or invalid
if seed_config_value is not None:
try:
seed_val = int(seed_config_value)
except (ValueError, TypeError):
logger.warning(
f"{self.log_prefix} 配置文件中的 default_seed ('{seed_config_value}') 无效,将使用默认种子 42。"
)
# seed_val is already 42
else:
logger.info(
f"{self.log_prefix} 未在配置中找到 default_seed将使用默认种子 42。建议在配置文件中添加 default_seed。"
)
# seed_val is already 42
# Watermark 现在完全由配置文件控制
effective_watermark_source = self.config.get("default_watermark", True) # 默认True
if isinstance(effective_watermark_source, bool):
watermark_val = effective_watermark_source
elif isinstance(effective_watermark_source, str):
watermark_val = effective_watermark_source.lower() == "true"
else:
logger.warning(
f"{self.log_prefix} 配置文件中的 default_watermark 值 '{effective_watermark_source}' 无效 (应为布尔值或 'true'/'false'),使用默认值 True。"
)
watermark_val = True
await self.send_message_by_expressor(
f"收到!正在为您生成关于 '{description}' 的图片,请稍候...(模型: {default_model}, 尺寸: {image_size}"
)
try:
success, result = await asyncio.to_thread(
self._make_http_image_request,
prompt=description,
model=default_model,
size=image_size,
seed=seed_val,
guidance_scale=guidance_scale_val,
watermark=watermark_val,
)
except Exception as e:
logger.error(f"{self.log_prefix} (HTTP) 异步请求执行失败: {e!r}", exc_info=True)
traceback.print_exc()
success = False
result = f"图片生成服务遇到意外问题: {str(e)[:100]}"
if success:
image_url = result
logger.info(f"{self.log_prefix} 图片URL获取成功: {image_url[:70]}... 下载并编码.")
try:
encode_success, encode_result = await asyncio.to_thread(self._download_and_encode_base64, image_url)
except Exception as e:
logger.error(f"{self.log_prefix} (B64) 异步下载/编码失败: {e!r}", exc_info=True)
traceback.print_exc()
encode_success = False
encode_result = f"图片下载或编码时发生内部错误: {str(e)[:100]}"
if encode_success:
base64_image_string = encode_result
send_success = await self.send_message(type="image", data=base64_image_string)
if send_success:
# 缓存成功的结果
self._request_cache[cache_key] = base64_image_string
self._cleanup_cache()
await self.send_message_by_expressor("图片表情已发送!")
return True, "图片表情已发送"
else:
await self.send_message_by_expressor("图片已处理为Base64但作为表情发送失败了。")
return False, "图片表情发送失败 (Base64)"
else:
await self.send_message_by_expressor(f"获取到图片URL但在处理图片时失败了{encode_result}")
return False, f"图片处理失败(Base64): {encode_result}"
else:
error_message = result
await self.send_message_by_expressor(f"哎呀,生成图片时遇到问题:{error_message}")
return False, f"图片生成失败: {error_message}"
def _download_and_encode_base64(self, image_url: str) -> Tuple[bool, str]:
"""下载图片并将其编码为Base64字符串"""
logger.info(f"{self.log_prefix} (B64) 下载并编码图片: {image_url[:70]}...")
try:
with urllib.request.urlopen(image_url, timeout=30) as response:
if response.status == 200:
image_bytes = response.read()
base64_encoded_image = base64.b64encode(image_bytes).decode("utf-8")
logger.info(f"{self.log_prefix} (B64) 图片下载编码完成. Base64长度: {len(base64_encoded_image)}")
return True, base64_encoded_image
else:
error_msg = f"下载图片失败 (状态: {response.status})"
logger.error(f"{self.log_prefix} (B64) {error_msg} URL: {image_url}")
return False, error_msg
except Exception as e: # Catches all exceptions from urlopen, b64encode, etc.
logger.error(f"{self.log_prefix} (B64) 下载或编码时错误: {e!r}", exc_info=True)
traceback.print_exc()
return False, f"下载或编码图片时发生错误: {str(e)[:100]}"
def _make_http_image_request(
self, prompt: str, model: str, size: str, seed: int | None, guidance_scale: float, watermark: bool
) -> Tuple[bool, str]:
base_url = self.config.get("base_url")
generate_api_key = self.config.get("volcano_generate_api_key")
endpoint = f"{base_url.rstrip('/')}/images/generations"
payload_dict = {
"model": model,
"prompt": prompt,
"response_format": "url",
"size": size,
"guidance_scale": guidance_scale,
"watermark": watermark,
"seed": seed, # seed is now always an int from process()
"api-key": generate_api_key,
}
# if seed is not None: # No longer needed, seed is always an int
# payload_dict["seed"] = seed
data = json.dumps(payload_dict).encode("utf-8")
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"Bearer {generate_api_key}",
}
logger.info(f"{self.log_prefix} (HTTP) 发起图片请求: {model}, Prompt: {prompt[:30]}... To: {endpoint}")
logger.debug(
f"{self.log_prefix} (HTTP) Request Headers: {{...Authorization: Bearer {generate_api_key[:10]}...}}"
)
logger.debug(
f"{self.log_prefix} (HTTP) Request Body (api-key omitted): {json.dumps({k: v for k, v in payload_dict.items() if k != 'api-key'})}"
)
req = urllib.request.Request(endpoint, data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=60) as response:
response_status = response.status
response_body_bytes = response.read()
response_body_str = response_body_bytes.decode("utf-8")
logger.info(f"{self.log_prefix} (HTTP) 响应: {response_status}. Preview: {response_body_str[:150]}...")
if 200 <= response_status < 300:
response_data = json.loads(response_body_str)
image_url = None
if (
isinstance(response_data.get("data"), list)
and response_data["data"]
and isinstance(response_data["data"][0], dict)
):
image_url = response_data["data"][0].get("url")
elif response_data.get("url"):
image_url = response_data.get("url")
if image_url:
logger.info(f"{self.log_prefix} (HTTP) 图片生成成功URL: {image_url[:70]}...")
return True, image_url
else:
logger.error(
f"{self.log_prefix} (HTTP) API成功但无图片URL. 响应预览: {response_body_str[:300]}..."
)
return False, "图片生成API响应成功但未找到图片URL"
else:
logger.error(
f"{self.log_prefix} (HTTP) API请求失败. 状态: {response.status}. 正文: {response_body_str[:300]}..."
)
return False, f"图片API请求失败(状态码 {response.status})"
except Exception as e:
logger.error(f"{self.log_prefix} (HTTP) 图片生成时意外错误: {e!r}", exc_info=True)
traceback.print_exc()
return False, f"图片生成HTTP请求时发生意外错误: {str(e)[:100]}"
def _validate_image_size(self, image_size: str) -> bool:
"""验证图片尺寸格式"""
try:
width, height = map(int, image_size.split("x"))
return 100 <= width <= 10000 and 100 <= height <= 10000
except (ValueError, TypeError):
return False

View File

@@ -0,0 +1,9 @@
base_url = "https://ark.cn-beijing.volces.com/api/v3"
volcano_generate_api_key = "YOUR_VOLCANO_GENERATE_API_KEY_HERE"
default_model = "doubao-seedream-3-0-t2i-250415"
default_size = "1024x1024"
default_watermark = true
default_guidance_scale = 2.5
default_seed = 42
cache_enabled = true
cache_max_size = 10

View File

@@ -0,0 +1,19 @@
# 火山方舟 API 的基础 URL
base_url = "https://ark.cn-beijing.volces.com/api/v3"
# 用于图片生成的API密钥
volcano_generate_api_key = "YOUR_VOLCANO_GENERATE_API_KEY_HERE"
# 默认图片生成模型
default_model = "doubao-seedream-3-0-t2i-250415"
# 默认图片尺寸
default_size = "1024x1024"
# 是否默认开启水印
default_watermark = true
# 默认引导强度
default_guidance_scale = 2.5
# 默认随机种子
default_seed = 42
# 更多插件特定配置可以在此添加...
# custom_parameter = "some_value"

View File

@@ -0,0 +1,74 @@
# 禁言插件配置文件
[plugin]
name = "mute_plugin"
version = "2.0.0"
enabled = true
description = "群聊禁言管理插件,提供智能禁言功能"
# 组件启用控制
[components]
enable_smart_mute = true # 启用智能禁言Action
enable_mute_command = true # 启用禁言命令Command
# 禁言配置
[mute]
# 时长限制(秒)
min_duration = 60 # 最短禁言时长
max_duration = 2592000 # 最长禁言时长30天
default_duration = 300 # 默认禁言时长5分钟
# 是否启用时长美化显示
enable_duration_formatting = true
# 是否记录禁言历史
log_mute_history = true
# 禁言消息模板
templates = [
"好的,禁言 {target} {duration},理由:{reason}",
"收到,对 {target} 执行禁言 {duration},因为{reason}",
"明白了,禁言 {target} {duration},原因是{reason}",
"✅ 已禁言 {target} {duration},理由:{reason}",
"🔇 对 {target} 执行禁言 {duration},因为{reason}",
"⛔ 禁言 {target} {duration},原因:{reason}"
]
# 错误消息模板
error_messages = [
"没有指定禁言对象呢~",
"没有指定禁言时长呢~",
"禁言时长必须是正数哦~",
"禁言时长必须是数字哦~",
"找不到 {target} 这个人呢~",
"查找用户信息时出现问题~"
]
# 智能禁言Action配置
[smart_mute]
# LLM判定严格模式
strict_mode = true
# 关键词激活设置
keyword_sensitivity = "normal" # low, normal, high
# 并行执行设置
allow_parallel = false
# 禁言命令配置
[mute_command]
# 是否需要管理员权限
require_admin = true
# 最大批量禁言数量
max_batch_size = 5
# 命令冷却时间(秒)
cooldown_seconds = 3
# 日志配置
[logging]
level = "INFO"
prefix = "[MutePlugin]"
include_user_info = true
include_duration_info = true

View File

@@ -0,0 +1,385 @@
"""
禁言插件
提供智能禁言功能的群聊管理插件。
功能特性:
- 智能LLM判定根据聊天内容智能判断是否需要禁言
- 灵活的时长管理:支持自定义禁言时长限制
- 模板化消息:支持自定义禁言提示消息
- 参数验证:完整的输入参数验证和错误处理
- 配置文件支持:所有设置可通过配置文件调整
包含组件:
- 智能禁言Action - 基于LLM判断是否需要禁言
- 禁言命令Command - 手动执行禁言操作
"""
from typing import List, Tuple, Type, Optional, Dict, Any
import random
# 导入新插件系统
from src.plugin_system.base.base_plugin import BasePlugin
from src.plugin_system.base.base_plugin import register_plugin
from src.plugin_system.base.base_action import BaseAction
from src.plugin_system.base.base_command import BaseCommand
from src.plugin_system.base.component_types import ComponentInfo, ActionActivationType, ChatMode
from src.common.logger_manager import get_logger
logger = get_logger("mute_plugin")
# ===== Action组件 =====
class MuteAction(BaseAction):
"""智能禁言Action - 基于LLM智能判断是否需要禁言"""
# Action基本信息
action_name = "mute"
action_description = "智能禁言系统基于LLM判断是否需要禁言"
# 激活设置
focus_activation_type = ActionActivationType.LLM_JUDGE # Focus模式使用LLM判定确保谨慎
normal_activation_type = ActionActivationType.KEYWORD # Normal模式使用关键词激活快速响应
# 关键词设置用于Normal模式
activation_keywords = ["禁言", "mute", "ban", "silence"]
keyword_case_sensitive = False
# LLM判定提示词用于Focus模式
llm_judge_prompt = """
判定是否需要使用禁言动作的严格条件:
使用禁言的情况:
1. 用户发送明显违规内容(色情、暴力、政治敏感等)
2. 恶意刷屏或垃圾信息轰炸
3. 用户主动明确要求被禁言("禁言我"等)
4. 严重违反群规的行为
5. 恶意攻击他人或群组管理
绝对不要使用的情况:
2. 情绪化表达但无恶意
3. 开玩笑或调侃,除非过分
4. 单纯的意见分歧或争论
"""
mode_enable = ChatMode.ALL
parallel_action = False
# Action参数定义
action_parameters = {
"target": "禁言对象,必填,输入你要禁言的对象的名字",
"duration": "禁言时长,必填,输入你要禁言的时长(秒),单位为秒,必须为数字",
"reason": "禁言理由,可选"
}
# Action使用场景
action_require = [
"当有人违反了公序良俗的内容",
"当有人刷屏时使用",
"当有人发了擦边,或者色情内容时使用",
"当有人要求禁言自己时使用",
"如果某人已经被禁言了,就不要再次禁言了,除非你想追加时间!!"
]
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行智能禁言判定"""
logger.info(f"{self.log_prefix} 执行智能禁言动作")
# 获取参数
target = self.action_data.get("target")
duration = self.action_data.get("duration")
reason = self.action_data.get("reason", "违反群规")
# 参数验证
if not target:
error_msg = "禁言目标不能为空"
logger.error(f"{self.log_prefix} {error_msg}")
await self.send_reply("没有指定禁言对象呢~")
return False, error_msg
if not duration:
error_msg = "禁言时长不能为空"
logger.error(f"{self.log_prefix} {error_msg}")
await self.send_reply("没有指定禁言时长呢~")
return False, error_msg
# 获取时长限制配置
min_duration = self.api.get_config("mute.min_duration", 60)
max_duration = self.api.get_config("mute.max_duration", 2592000)
# 验证时长格式并转换
try:
duration_int = int(duration)
if duration_int <= 0:
error_msg = "禁言时长必须大于0"
logger.error(f"{self.log_prefix} {error_msg}")
await self.send_reply("禁言时长必须是正数哦~")
return False, error_msg
# 限制禁言时长范围
if duration_int < min_duration:
duration_int = min_duration
logger.info(f"{self.log_prefix} 禁言时长过短,调整为{min_duration}")
elif duration_int > max_duration:
duration_int = max_duration
logger.info(f"{self.log_prefix} 禁言时长过长,调整为{max_duration}")
except (ValueError, TypeError):
error_msg = f"禁言时长格式无效: {duration}"
logger.error(f"{self.log_prefix} {error_msg}")
await self.send_reply("禁言时长必须是数字哦~")
return False, error_msg
# 获取用户ID
try:
platform, user_id = await self.api.get_user_id_by_person_name(target)
except Exception as e:
error_msg = f"查找用户ID时出错: {e}"
logger.error(f"{self.log_prefix} {error_msg}")
await self.send_reply("查找用户信息时出现问题~")
return False, error_msg
if not user_id:
error_msg = f"未找到用户 {target} 的ID"
await self.send_reply(f"找不到 {target} 这个人呢~")
logger.error(f"{self.log_prefix} {error_msg}")
return False, error_msg
# 格式化时长显示
enable_formatting = self.api.get_config("mute.enable_duration_formatting", True)
time_str = self._format_duration(duration_int) if enable_formatting else f"{duration_int}"
# 获取模板化消息
message = self._get_template_message(target, time_str, reason)
await self.send_reply(message)
# 发送群聊禁言命令
success = await self.send_command(
command_name="GROUP_BAN",
args={"qq_id": str(user_id), "duration": str(duration_int)},
display_message=f"禁言了 {target} {time_str}"
)
if success:
logger.info(f"{self.log_prefix} 成功发送禁言命令,用户 {target}({user_id}),时长 {duration_int}")
return True, f"成功禁言 {target},时长 {time_str}"
else:
error_msg = "发送禁言命令失败"
logger.error(f"{self.log_prefix} {error_msg}")
await self.send_reply(f"执行禁言动作失败")
return False, error_msg
def _get_template_message(self, target: str, duration_str: str, reason: str) -> str:
"""获取模板化的禁言消息"""
templates = self.api.get_config("mute.templates", [
"好的,禁言 {target} {duration},理由:{reason}",
"收到,对 {target} 执行禁言 {duration},因为{reason}",
"明白了,禁言 {target} {duration},原因是{reason}"
])
template = random.choice(templates)
return template.format(target=target, duration=duration_str, reason=reason)
def _format_duration(self, seconds: int) -> str:
"""将秒数格式化为可读的时间字符串"""
if seconds < 60:
return f"{seconds}"
elif seconds < 3600:
minutes = seconds // 60
remaining_seconds = seconds % 60
if remaining_seconds > 0:
return f"{minutes}{remaining_seconds}"
else:
return f"{minutes}分钟"
elif seconds < 86400:
hours = seconds // 3600
remaining_minutes = (seconds % 3600) // 60
if remaining_minutes > 0:
return f"{hours}小时{remaining_minutes}分钟"
else:
return f"{hours}小时"
else:
days = seconds // 86400
remaining_hours = (seconds % 86400) // 3600
if remaining_hours > 0:
return f"{days}{remaining_hours}小时"
else:
return f"{days}"
# ===== Command组件 =====
class MuteCommand(BaseCommand):
"""禁言命令 - 手动执行禁言操作"""
# Command基本信息
command_name = "mute_command"
command_description = "禁言命令,手动执行禁言操作"
command_pattern = r"^/mute\s+(?P<target>\S+)\s+(?P<duration>\d+)(?:\s+(?P<reason>.+))?$"
command_help = "禁言指定用户,用法:/mute <用户名> <时长(秒)> [理由]"
command_examples = [
"/mute 用户名 300",
"/mute 张三 600 刷屏",
"/mute @某人 1800 违规内容"
]
intercept_message = True # 拦截消息处理
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行禁言命令"""
try:
target = self.matched_groups.get("target")
duration = self.matched_groups.get("duration")
reason = self.matched_groups.get("reason", "管理员操作")
if not all([target, duration]):
await self.send_reply("❌ 命令参数不完整,请检查格式")
return False, "参数不完整"
# 获取时长限制配置
min_duration = self.api.get_config("mute.min_duration", 60)
max_duration = self.api.get_config("mute.max_duration", 2592000)
# 验证时长
try:
duration_int = int(duration)
if duration_int <= 0:
await self.send_reply("❌ 禁言时长必须大于0")
return False, "时长无效"
# 限制禁言时长范围
if duration_int < min_duration:
duration_int = min_duration
await self.send_reply(f"⚠️ 禁言时长过短,调整为{min_duration}")
elif duration_int > max_duration:
duration_int = max_duration
await self.send_reply(f"⚠️ 禁言时长过长,调整为{max_duration}")
except ValueError:
await self.send_reply("❌ 禁言时长必须是数字")
return False, "时长格式错误"
# 获取用户ID
try:
platform, user_id = await self.api.get_user_id_by_person_name(target)
except Exception as e:
logger.error(f"{self.log_prefix} 查找用户ID时出错: {e}")
await self.send_reply("❌ 查找用户信息时出现问题")
return False, str(e)
if not user_id:
await self.send_reply(f"❌ 找不到用户: {target}")
return False, "用户不存在"
# 格式化时长显示
enable_formatting = self.api.get_config("mute.enable_duration_formatting", True)
time_str = self._format_duration(duration_int) if enable_formatting else f"{duration_int}"
logger.info(f"{self.log_prefix} 执行禁言命令: {target}({user_id}) -> {time_str}")
# 发送群聊禁言命令
success = await self.send_command(
command_name="GROUP_BAN",
args={"qq_id": str(user_id), "duration": str(duration_int)},
display_message=f"禁言了 {target} {time_str}"
)
if success:
# 获取并发送模板化消息
message = self._get_template_message(target, time_str, reason)
await self.send_reply(message)
logger.info(f"{self.log_prefix} 成功禁言 {target}({user_id}),时长 {duration_int}")
return True, f"成功禁言 {target},时长 {time_str}"
else:
await self.send_reply("❌ 发送禁言命令失败")
return False, "发送禁言命令失败"
except Exception as e:
logger.error(f"{self.log_prefix} 禁言命令执行失败: {e}")
await self.send_reply(f"❌ 禁言命令错误: {str(e)}")
return False, str(e)
def _get_template_message(self, target: str, duration_str: str, reason: str) -> str:
"""获取模板化的禁言消息"""
templates = self.api.get_config("mute.templates", [
"✅ 已禁言 {target} {duration},理由:{reason}",
"🔇 对 {target} 执行禁言 {duration},因为{reason}",
"⛔ 禁言 {target} {duration},原因:{reason}"
])
template = random.choice(templates)
return template.format(target=target, duration=duration_str, reason=reason)
def _format_duration(self, seconds: int) -> str:
"""将秒数格式化为可读的时间字符串"""
if seconds < 60:
return f"{seconds}"
elif seconds < 3600:
minutes = seconds // 60
remaining_seconds = seconds % 60
if remaining_seconds > 0:
return f"{minutes}{remaining_seconds}"
else:
return f"{minutes}分钟"
elif seconds < 86400:
hours = seconds // 3600
remaining_minutes = (seconds % 3600) // 60
if remaining_minutes > 0:
return f"{hours}小时{remaining_minutes}分钟"
else:
return f"{hours}小时"
else:
days = seconds // 86400
remaining_hours = (seconds % 86400) // 3600
if remaining_hours > 0:
return f"{days}{remaining_hours}小时"
else:
return f"{days}"
# ===== 插件主类 =====
@register_plugin
class MutePlugin(BasePlugin):
"""禁言插件
提供智能禁言功能:
- 智能禁言Action基于LLM判断是否需要禁言
- 禁言命令Command手动执行禁言操作
"""
# 插件基本信息
plugin_name = "mute_plugin"
plugin_description = "群聊禁言管理插件,提供智能禁言功能"
plugin_version = "2.0.0"
plugin_author = "MaiBot开发团队"
enable_plugin = True
config_file_name = "config.toml"
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表"""
# 从配置获取组件启用状态
enable_smart_mute = self.get_config("components.enable_smart_mute", True)
enable_mute_command = self.get_config("components.enable_mute_command", True)
components = []
# 添加智能禁言Action
if enable_smart_mute:
components.append((
MuteAction.get_action_info(),
MuteAction
))
# 添加禁言命令Command
if enable_mute_command:
components.append((
MuteCommand.get_command_info(),
MuteCommand
))
return components

View File

@@ -0,0 +1 @@
from . import tts_action # noqa

View File

@@ -0,0 +1,84 @@
from src.common.logger_manager import get_logger
from src.chat.actions.base_action import ActionActivationType
from src.chat.actions.plugin_action import PluginAction, register_action
from typing import Tuple
logger = get_logger("tts_action")
@register_action
class TTSAction(PluginAction):
"""TTS语音转换动作处理类"""
action_name = "tts_action"
action_description = "将文本转换为语音进行播放,适用于需要语音输出的场景"
action_parameters = {
"text": "需要转换为语音的文本内容,必填,内容应当适合语音播报,语句流畅、清晰",
}
action_require = [
"当需要发送语音信息时使用",
"当用户明确要求使用语音功能时使用",
"当表达内容更适合用语音而不是文字传达时使用",
"当用户想听到语音回答而非阅读文本时使用",
]
enable_plugin = True # 启用插件
associated_types = ["tts_text"]
focus_activation_type = ActionActivationType.LLM_JUDGE
normal_activation_type = ActionActivationType.KEYWORD
# 关键词配置 - Normal模式下使用关键词触发
activation_keywords = ["语音", "tts", "播报", "读出来", "语音播放", "", "朗读"]
keyword_case_sensitive = False
# 并行执行设置 - TTS可以与回复并行执行不覆盖回复内容
parallel_action = False
async def process(self) -> Tuple[bool, str]:
"""处理TTS文本转语音动作"""
logger.info(f"{self.log_prefix} 执行TTS动作: {self.reasoning}")
# 获取要转换的文本
text = self.action_data.get("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_message(type="tts_text", data=processed_text)
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)
# 确保句子结尾有合适的标点
if not any(processed_text.endswith(end) for end in [".", "?", "!", "", "", ""]):
processed_text = processed_text + ""
return processed_text

View File

@@ -0,0 +1 @@
from . import vtb_action # noqa

View File

@@ -0,0 +1,96 @@
from src.common.logger_manager import get_logger
from src.chat.actions.plugin_action import PluginAction, register_action, ActionActivationType
from typing import Tuple
logger = get_logger("vtb_action")
@register_action
class VTBAction(PluginAction):
"""VTB虚拟主播动作处理类"""
action_name = "vtb_action"
action_description = "使用虚拟主播预设动作表达心情或感觉,适用于需要生动表达情感的场景"
action_parameters = {
"text": "描述想要表达的心情或感觉的文本内容,必填,应当是对情感状态的自然描述",
}
action_require = [
"当需要表达特定情感或心情时使用",
"当用户明确要求使用虚拟主播动作时使用",
"当回应内容需要更生动的情感表达时使用",
"当想要通过预设动作增强互动体验时使用",
]
enable_plugin = True # 启用插件
associated_types = ["vtb_text"]
# 激活类型设置
focus_activation_type = ActionActivationType.LLM_JUDGE # Focus模式使用LLM判定精确识别情感表达需求
normal_activation_type = ActionActivationType.RANDOM # Normal模式使用随机激活增加趣味性
# LLM判定提示词用于Focus模式
llm_judge_prompt = """
判定是否需要使用VTB虚拟主播动作的条件
1. 当前聊天内容涉及明显的情感表达需求
2. 用户询问或讨论情感相关话题
3. 场景需要生动的情感回应
4. 当前回复内容可以通过VTB动作增强表达效果
不需要使用的情况:
1. 纯粹的信息查询
2. 技术性问题讨论
3. 不涉及情感的日常对话
4. 已经有足够的情感表达
"""
# Random激活概率用于Normal模式
random_activation_probability = 0.08 # 较低概率,避免过度使用
async def process(self) -> Tuple[bool, str]:
"""处理VTB虚拟主播动作"""
logger.info(f"{self.log_prefix} 执行VTB动作: {self.reasoning}")
# 获取要表达的心情或感觉文本
text = self.action_data.get("text")
if not text:
logger.error(f"{self.log_prefix} 执行VTB动作时未提供文本内容")
return False, "执行VTB动作失败未提供文本内容"
# 处理文本使其更适合VTB动作表达
processed_text = self._process_text_for_vtb(text)
try:
# 发送VTB动作消息
await self.send_message(type="vtb_text", data=processed_text)
logger.info(f"{self.log_prefix} VTB动作执行成功文本内容: {processed_text}")
return True, "VTB动作执行成功"
except Exception as e:
logger.error(f"{self.log_prefix} 执行VTB动作时出错: {e}")
return False, f"执行VTB动作时出错: {e}"
def _process_text_for_vtb(self, text: str) -> str:
"""
处理文本使其更适合VTB动作表达
- 优化情感表达的准确性
- 规范化心情描述格式
- 确保文本适合虚拟主播动作系统理解
"""
# 简单示例实现
processed_text = text.strip()
# 移除多余的空格和换行
import re
processed_text = re.sub(r"\s+", " ", processed_text)
# 确保文本长度适中,避免过长的描述
if len(processed_text) > 100:
processed_text = processed_text[:100] + "..."
# 如果文本为空,提供默认的情感描述
if not processed_text:
processed_text = "平静"
return processed_text