This commit is contained in:
Windpicker-owo
2025-11-28 13:26:01 +08:00
9 changed files with 199 additions and 42 deletions

View File

@@ -1,5 +1,9 @@
import base64
import io
from enum import Enum
from PIL import Image
# 设计这系列类的目的是为未来可能的扩展做准备
@@ -53,6 +57,35 @@ class MessageBuilder:
self.__content.append(text)
return self
def _convert_gif_to_png_frames(self, gif_base64: str, max_frames: int = 4) -> list[str]:
"""将GIF的Base64编码分解为多个PNG帧的Base64编码列表"""
gif_bytes = base64.b64decode(gif_base64)
gif_image = Image.open(io.BytesIO(gif_bytes))
frames = []
total_frames = getattr(gif_image, "n_frames", 1)
# 如果总帧数小于等于最大帧数,则全部提取
if total_frames <= max_frames:
indices = range(total_frames)
else:
# 否则,在总帧数中均匀选取 max_frames 帧
indices = [int(i * (total_frames - 1) / (max_frames - 1)) for i in range(max_frames)]
for i in indices:
try:
gif_image.seek(i)
frame = gif_image.convert("RGBA")
output_buffer = io.BytesIO()
frame.save(output_buffer, format="PNG")
png_bytes = output_buffer.getvalue()
frames.append(base64.b64encode(png_bytes).decode("utf-8"))
except EOFError:
# 到达文件末尾,停止提取
break
return frames
def add_image_content(
self,
image_format: str,
@@ -60,18 +93,35 @@ class MessageBuilder:
support_formats=None, # 默认支持格式
) -> "MessageBuilder":
"""
添加图片内容
添加图片内容, 如果是GIF且模型不支持, 则会分解为最多4帧PNG图片。
:param image_format: 图片格式
:param image_base64: 图片的base64编码
:return: MessageBuilder对象
"""
if support_formats is None:
support_formats = SUPPORTED_IMAGE_FORMATS
if image_format.lower() not in support_formats:
raise ValueError("不受支持的图片格式")
current_format = image_format.lower()
# 如果是GIF且模型不支持, 则分解为多个PNG帧
if current_format == "gif" and "gif" not in support_formats:
if "png" in support_formats:
png_frames_base64 = self._convert_gif_to_png_frames(image_base64)
for frame_base64 in png_frames_base64:
if not frame_base64:
continue
self.__content.append(("png", frame_base64))
return self
else:
raise ValueError("模型不支持GIF, 且无法转换为PNG")
# 对于其他格式或模型支持GIF的情况
if current_format not in support_formats:
raise ValueError(f"不受支持的图片格式: {current_format}")
if not image_base64:
raise ValueError("图片的base64编码不能为空")
self.__content.append((image_format, image_base64))
self.__content.append((current_format, image_base64))
return self
def add_tool_call(self, tool_call_id: str) -> "MessageBuilder":

View File

@@ -360,7 +360,7 @@ class MainSystem:
async def initialize(self) -> None:
"""初始化系统组件"""
# 检查必要的配置
if not hasattr(global_config, "bot") or not hasattr(global_config.bot, "nickname"):
if not global_config or not global_config.bot or not global_config.bot.nickname:
logger.error("缺少必要的bot配置")
raise ValueError("Bot配置不完整")
@@ -386,7 +386,7 @@ class MainSystem:
selected_egg = choices(egg_texts, weights=weights, k=1)[0]
logger.info(f"""
全部系统初始化完成,{global_config.bot.nickname}已成功唤醒
全部系统初始化完成,{global_config.bot.nickname if global_config and global_config.bot else 'Bot'}已成功唤醒
=========================================================
MoFox_Bot(第三方修改版)
全部组件已成功启动!
@@ -484,7 +484,7 @@ MoFox_Bot(第三方修改版)
# 初始化三层记忆系统(如果启用)
try:
if global_config.memory and global_config.memory.enable:
if global_config and global_config.memory and global_config.memory.enable:
from src.memory_graph.manager_singleton import initialize_unified_memory_manager
logger.info("三层记忆系统已启用,正在初始化...")
await initialize_unified_memory_manager()
@@ -568,7 +568,7 @@ MoFox_Bot(第三方修改版)
async def _init_planning_components(self) -> None:
"""初始化计划相关组件"""
# 初始化月度计划管理器
if global_config.planning_system.monthly_plan_enable:
if global_config and global_config.planning_system and global_config.planning_system.monthly_plan_enable:
try:
await monthly_plan_manager.start_monthly_plan_generation()
logger.info("月度计划管理器初始化成功")
@@ -576,7 +576,7 @@ MoFox_Bot(第三方修改版)
logger.error(f"月度计划管理器初始化失败: {e}")
# 初始化日程管理器
if global_config.planning_system.schedule_enable:
if global_config and global_config.planning_system and global_config.planning_system.schedule_enable:
try:
await schedule_manager.load_or_generate_today_schedule()
await schedule_manager.start_daily_schedule_generation()

View File

@@ -50,7 +50,7 @@ async def initialize_memory_manager(
from src.config.config import global_config
# 检查是否启用
if not global_config.memory or not getattr(global_config.memory, "enable", False):
if not global_config or not global_config.memory or not getattr(global_config.memory, "enable", False):
logger.info("记忆图系统已在配置中禁用")
_initialized = False
_memory_manager = None
@@ -58,7 +58,7 @@ async def initialize_memory_manager(
# 处理数据目录
if data_dir is None:
data_dir = getattr(global_config.memory, "data_dir", "data/memory_graph")
data_dir = getattr(global_config.memory, "data_dir", "data/memory_graph") if global_config and global_config.memory else "data/memory_graph"
if isinstance(data_dir, str):
data_dir = Path(data_dir)
@@ -136,12 +136,15 @@ async def initialize_unified_memory_manager():
from src.memory_graph.unified_manager import UnifiedMemoryManager
# 检查是否启用三层记忆系统
if not hasattr(global_config, "memory") or not getattr(
if not global_config or not global_config.memory or not getattr(
global_config.memory, "enable", False
):
logger.warning("三层记忆系统未启用,跳过初始化")
return None
if not global_config or not global_config.memory:
logger.warning("未找到内存配置,跳过统一内存管理器初始化。")
return None
config = global_config.memory
# 创建管理器实例

View File

@@ -330,7 +330,11 @@ class UnifiedMemoryManager:
"""
prompt = f"""你是一个记忆检索评估专家。请判断检索到的记忆是否足以回答用户的问题
prompt = f"""你是一个记忆检索评估专家。你的任务是判断当前检索到的“感知记忆”(即时对话)和“短期记忆”(结构化信息)是否足以支撑一次有深度、有上下文的回复
**核心原则:**
- **不要轻易检索长期记忆!** 只有在当前对话需要深入探讨、回忆过去复杂事件或需要特定背景知识时,才认为记忆不足。
- **闲聊、简单问候、表情互动或无特定主题的对话,现有记忆通常是充足的。** 频繁检索长期记忆会拖慢响应速度。
**用户查询:**
{query}
@@ -341,25 +345,32 @@ class UnifiedMemoryManager:
**检索到的短期记忆(结构化信息,自然语言描述):**
{short_term_desc or '(无)'}
**任务要求**
1. 判断这些记忆是否足以回答用户的问题
2. 如果不充足,分析缺少哪些方面的信息
3. 生成额外需要检索的 query用于在长期记忆中检索
**评估指南**
1. **分析用户意图**:用户是在闲聊,还是在讨论一个需要深入挖掘的话题?
2. **检查现有记忆**:当前的感知和短期记忆是否已经包含了足够的信息来回应用户的查询?
- 对于闲聊(如“你好”、“哈哈”、“[表情]”),现有记忆总是充足的 (`"is_sufficient": true`)。
- 对于需要回忆具体细节、深入探讨个人经历或专业知识的查询,如果现有记忆中没有相关信息,则可能不充足。
3. **决策**
- 如果记忆充足,设置 `"is_sufficient": true`。
- 如果确实需要更多信息才能进行有意义的对话,设置 `"is_sufficient": false`,并提供具体的补充查询。
**输出格式JSON**
```json
{{
"is_sufficient": true/false,
"confidence": 0.85,
"reasoning": "判断理由",
"reasoning": "在这里解释你的判断理由。例如:‘用户只是在打招呼,现有记忆已足够’或‘用户问到了一个具体的历史事件,需要检索长期记忆’。",
"missing_aspects": ["缺失的信息1", "缺失的信息2"],
"additional_queries": ["补充query1", "补充query2"]
}}
```
请输出JSON"""
严格按照上述原则进行判断,并输出JSON"""
# 调用记忆裁判模型
from src.config.config import model_config
if not model_config.model_task_config:
raise ValueError("模型任务配置未加载")
llm = LLMRequest(
model_set=model_config.model_task_config.memory_judge,
request_type="unified_memory.judge",

View File

@@ -60,10 +60,9 @@ decision_prompt_template_group = Prompt(
- 心情影响:心情会影响你冒泡的方式和内容。
**选项3发起一次有目的的互动 (throw_topic)**
- 适用场景:你想延续对话表达关心、或深入讨论某个具体话题
- 适用场景:你想延续对话表达关心。
- **【互动类型1延续约定或提醒】(最高优先级)**:检查最近的聊天记录,是否存在可以延续的互动。例如,如果昨晚的最后一条消息是“晚安”,现在是早上,一个“早安”的回应是绝佳的选择。如果之前提到过某个约定(如“待会聊”),现在可以主动跟进。
- **【互动类型2展现真诚的关心】(次高优先级)**:如果不存在可延续的约定,请仔细阅读聊天记录,寻找**群友**提及的个人状况(如天气、出行、身体、情绪、工作学习等),并主动表达关心。
- **【互动类型3开启新话题】**:当以上两点都不适用时,可以考虑开启一个你感兴趣的新话题。
- 心情影响:心情会影响你想发起互动的方式和内容。
请以JSON格式回复你的决策
@@ -138,14 +137,9 @@ throw_topic_reply_prompt_template_group = Prompt(
- 如果意图是**延续约定**(如回应“晚安”),请直接生成对应的问候。
- 如果意图是**表达关心**(如跟进群友提到的事),请生成自然、真诚的关心话语。
- 如果意图是**开启新话题**:请严格遵守以下“新话题构思三步法”:
1. **寻找灵感****首选**从【最近的聊天记录】中寻找一个可以自然延续的**生活化**细节。**严禁**引入与聊天记录完全无关的、凭空出现的话题。如果记录为空,可以根据你的【人设】,提出一个**非常普适、开放式**的生活化问题或感想。
2. **确定风格**:请**确保新话题与最近的聊天内容有关联**,自然地引入话题,避免过于跳脱。
3. **最终检查**:你提出的话题是否合理?是否贴近现实和聊天内容?说话方式是否正常?是否像一个真正的人类?
请根据这个意图,生成一条消息,要求:
1. 要与最近的聊天记录相关,自然地引入话题或表达关心。
2. 长度适中(20-40字)。
2. 长度适中(15-25字左右)。
3. 结合最近的聊天记录确保对话连贯,不要显得突兀。
4. 符合你的人设和当前聊天风格。
5. **你的心情会影响你的表达方式**。
@@ -189,10 +183,9 @@ decision_prompt_template_private = Prompt(
- 心情影响:心情会影响你问候的方式和内容。
**选项3发起一次有目的的互动 (throw_topic)**
- 适用场景:你想延续对话表达关心、或深入讨论某个具体话题
- 适用场景:你想延续对话表达关心。
- **【互动类型1延续约定或提醒】(最高优先级)**:检查最近的聊天记录,是否存在可以延续的互动。例如,如果昨晚的最后一条消息是“晚安”,现在是早上,一个“早安”的回应是绝佳的选择。如果之前提到过某个约定(如“待会聊”),现在可以主动跟进。
- **【互动类型2展现真诚的关心】(次高优先级)**:如果不存在可延续的约定,请仔细阅读聊天记录,寻找**对方**提及的个人状况(如天气、出行、身体、情绪、工作学习等),并主动表达关心。
- **【互动类型3开启新话题】**:当以上两点都不适用时,可以考虑开启一个你感兴趣的新话题。
- 心情影响:心情会影响你想发起互动的方式和内容。
请以JSON格式回复你的决策
@@ -266,14 +259,9 @@ throw_topic_reply_prompt_template_private = Prompt(
请根据你的互动意图,并参考最近的聊天记录,生成一条有温度的、**适合在私聊中说**的消息。
- 如果意图是**延续约定**(如回应“晚安”),请直接生成对应的问候。
- 如果意ت意图是**表达关心**(如跟进对方提到的事),请生成自然、真诚的关心话语。
- 如果意图是**开启新话题**:请严格遵守以下“新话题构思三步法”:
1. **寻找灵感****首选**从【最近的聊天记录】中寻找一个可以自然延续的**生活化**细节。**严禁**引入与聊天记录完全无关的、凭空出现的话题。如果记录为空,可以根据你的【人设】,提出一个**非常普适、开放式**的生活化问题或感想。
2. **确定风格**:请**确保新话题与最近的聊天内容有关联**,自然地引入话题,避免过于跳脱。
3. **最终检查**:你提出的话题是否合理?是否贴近现实和聊天内容?说话方式是否正常?是否像一个真正的人类?
请根据这个意图,生成一条消息,要求:
1. 要与最近的聊天记录相关,自然地引入话题或表达关心。
2. 长度适中(20-40字)。
2. 长度适中(15-25字左右)。
3. 结合最近的聊天记录确保对话连贯,不要显得突兀。
4. 符合你的人设和当前聊天风格。
5. **你的心情会影响你的表达方式**。

View File

@@ -317,6 +317,9 @@ class NapcatAdapterPlugin(BasePlugin):
"ignore_non_self_poke": ConfigField(type=bool, default=False, description="是否忽略不是针对自己的戳一戳消息"),
"poke_debounce_seconds": ConfigField(type=float, default=2.0, description="戳一戳防抖时间(秒)"),
"enable_emoji_like": ConfigField(type=bool, default=True, description="是否启用群聊表情回复处理"),
"enable_reply_at": ConfigField(type=bool, default=True, description="是否在回复时自动@原消息发送者"),
"reply_at_rate": ConfigField(type=float, default=0.5, description="回复时@的概率0.0-1.0"),
"enable_video_processing": ConfigField(type=bool, default=True, description="是否启用视频消息处理(下载和解析)"),
},
}

View File

@@ -214,6 +214,9 @@ class MessageHandler:
case RealMessageType.record:
return await self._handle_record_message(segment)
case RealMessageType.video:
if not config_api.get_plugin_config(self.plugin_config, "features.enable_video_processing", False):
logger.debug("视频消息处理已禁用,跳过")
return {"type": "text", "data": "[视频消息]"}
return await self._handle_video_message(segment)
case RealMessageType.rps:
return await self._handle_rps_message(segment)

View File

@@ -370,14 +370,8 @@ class SendHandler:
def handle_voice_message(self, encoded_voice: str) -> dict:
"""处理语音消息"""
use_tts = False
if self.plugin_config:
use_tts = config_api.get_plugin_config(self.plugin_config, "voice.use_tts", False)
if not use_tts:
logger.warning("未启用语音消息处理")
return {}
if not encoded_voice:
logger.warning("接收到空的语音消息,跳过处理")
return {}
return {
"type": "record",

View File

@@ -50,6 +50,106 @@ class TTSVoicePlugin(BasePlugin):
super().__init__(*args, **kwargs)
self.tts_service = None
def _create_default_config(self, config_file: Path):
"""
如果配置文件不存在,则创建一个默认的配置文件。
"""
if config_file.is_file():
return
logger.info(f"TTS 配置文件不存在,正在创建默认配置文件于: {config_file}")
default_config_content = """# 插件基础配置
[plugin]
enable = true
keywords = [
"发语音", "语音", "说句话", "用语音说", "听你", "听声音", "想听你", "想听声音",
"讲个话", "说段话", "念一下", "读一下", "用嘴说", "", "能发语音吗","亲口"
]
# 组件启用控制
[components]
action_enabled = true
command_enabled = true
# TTS 语音合成基础配置
[tts]
server = "http://127.0.0.1:9880"
timeout = 180
max_text_length = 1000
# TTS 风格参数配置
# 每个 [[tts_styles]] 代表一个独立的语音风格配置
[[tts_styles]]
# 风格的唯一标识符,必须有一个名为 "default"
style_name = "default"
# 显示名称
name = "默认"
# 参考音频路径
refer_wav_path = "C:/path/to/your/reference.wav"
# 参考音频文本
prompt_text = "这是一个示例文本,请替换为您自己的参考音频文本。"
# 参考音频语言
prompt_language = "zh"
# GPT 模型路径
gpt_weights = "C:/path/to/your/gpt_weights.ckpt"
# SoVITS 模型路径
sovits_weights = "C:/path/to/your/sovits_weights.pth"
# 语速
speed_factor = 1.0
# TTS 高级参数配置
[tts_advanced]
media_type = "wav"
top_k = 9
top_p = 0.8
temperature = 0.8
batch_size = 6
batch_threshold = 0.75
text_split_method = "cut5"
repetition_penalty = 1.4
sample_steps = 150
super_sampling = true
# 空间音效配置
[spatial_effects]
# 是否启用空间音效处理
enabled = false
# 是否启用标准混响效果
reverb_enabled = false
# 混响的房间大小 (建议范围 0.0-1.0)
room_size = 0.2
# 混响的阻尼/高频衰减 (建议范围 0.0-1.0)
damping = 0.6
# 混响的湿声(效果声)比例 (建议范围 0.0-1.0)
wet_level = 0.3
# 混响的干声(原声)比例 (建议范围 0.0-1.0)
dry_level = 0.8
# 混响的立体声宽度 (建议范围 0.0-1.0)
width = 1.0
# 是否启用卷积混响需要assets/small_room_ir.wav文件
convolution_enabled = false
# 卷积混响的干湿比 (建议范围 0.0-1.0)
convolution_mix = 0.7
"""
try:
config_file.parent.mkdir(parents=True, exist_ok=True)
with open(config_file, "w", encoding="utf-8") as f:
f.write(default_config_content.strip())
logger.info("默认 TTS 配置文件创建成功。")
except Exception as e:
logger.error(f"创建默认 TTS 配置文件失败: {e}")
def _get_config_wrapper(self, key: str, default: Any = None) -> Any:
"""
配置获取的包装器,用于解决 get_config 无法直接获取动态表(如 tts_styles和未在 schema 中定义的节的问题。
@@ -93,6 +193,11 @@ class TTSVoicePlugin(BasePlugin):
"""
logger.info("初始化 TTSVoicePlugin...")
plugin_file = Path(__file__).resolve()
bot_root = plugin_file.parent.parent.parent.parent.parent
config_file = bot_root / "config" / "plugins" / self.plugin_name / self.config_file_name
self._create_default_config(config_file)
# 实例化 TTSService并传入 get_config 方法
self.tts_service = TTSService(self._get_config_wrapper)