Merge branch 'dev' of https://github.com/MoFox-Studio/MoFox_Bot into dev
This commit is contained in:
@@ -108,11 +108,6 @@ class ChatterManager:
|
||||
|
||||
self.stats["streams_processed"] += 1
|
||||
try:
|
||||
# 设置触发用户ID
|
||||
last_message = context.get_last_message()
|
||||
if last_message:
|
||||
context.triggering_user_id = last_message.user_info.user_id
|
||||
|
||||
result = await self.instances[stream_id].execute(context)
|
||||
|
||||
# 检查执行结果是否真正成功
|
||||
|
||||
@@ -878,7 +878,7 @@ class MemorySystem:
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"检索瞬时记忆失败: {e}", exc_info=True)
|
||||
|
||||
|
||||
# 最终截断
|
||||
final_memories = final_memories[:effective_limit]
|
||||
|
||||
|
||||
@@ -72,4 +72,4 @@ class MessageCollectionProcessor:
|
||||
"active_buffers": len(self.message_buffers),
|
||||
"total_buffered_messages": total_buffered_messages,
|
||||
"buffer_capacity_per_chat": self.buffer_size,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
专用于存储和检索消息集合,以提供即时上下文。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
@@ -125,7 +124,7 @@ class MessageCollectionStorage:
|
||||
if results and results.get("ids") and results["ids"][0]:
|
||||
for metadata in results["metadatas"][0]:
|
||||
collections.append(MessageCollection.from_dict(metadata))
|
||||
|
||||
|
||||
return collections
|
||||
except Exception as e:
|
||||
logger.error(f"检索相关消息集合失败: {e}", exc_info=True)
|
||||
@@ -163,7 +162,7 @@ class MessageCollectionStorage:
|
||||
|
||||
# 格式化消息集合为 prompt 上下文
|
||||
final_context = "\n\n---\n\n".join(context_parts) + "\n\n---"
|
||||
|
||||
|
||||
logger.info(f"🔗 为查询 '{query_text[:50]}...' 在聊天 '{chat_id}' 中找到 {len(collections)} 个相关消息集合上下文")
|
||||
return f"\n{final_context}\n"
|
||||
|
||||
@@ -192,4 +191,4 @@ class MessageCollectionStorage:
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取消息集合存储统计失败: {e}")
|
||||
return {}
|
||||
return {}
|
||||
|
||||
@@ -381,6 +381,11 @@ class StreamLoopManager:
|
||||
if cached_messages:
|
||||
logger.info(f"处理开始前刷新缓存消息: stream={stream_id}, 数量={len(cached_messages)}")
|
||||
|
||||
# 设置触发用户ID,以实现回复保护
|
||||
last_message = context.get_last_message()
|
||||
if last_message:
|
||||
context.triggering_user_id = last_message.user_info.user_id
|
||||
|
||||
# 创建子任务用于刷新能量(不阻塞主流程)
|
||||
energy_task = asyncio.create_task(self._refresh_focus_energy(stream_id))
|
||||
child_tasks.add(energy_task)
|
||||
|
||||
@@ -156,6 +156,13 @@ class ChatStream:
|
||||
|
||||
return instance
|
||||
|
||||
def get_raw_id(self) -> str:
|
||||
"""获取原始的、未哈希的聊天流ID字符串"""
|
||||
if self.group_info:
|
||||
return f"{self.platform}:{self.group_info.group_id}:group"
|
||||
else:
|
||||
return f"{self.platform}:{self.user_info.user_id}:private"
|
||||
|
||||
def update_active_time(self):
|
||||
"""更新最后活跃时间"""
|
||||
self.last_active_time = time.time()
|
||||
@@ -256,18 +263,18 @@ class ChatStream:
|
||||
def _prepare_additional_config(self, message_info) -> str | None:
|
||||
"""
|
||||
准备 additional_config,将 format_info 嵌入其中
|
||||
|
||||
|
||||
这个方法模仿 storage.py 中的逻辑,确保 DatabaseMessages 中的 additional_config
|
||||
包含 format_info,使得 action_modifier 能够正确获取适配器支持的消息类型
|
||||
|
||||
|
||||
Args:
|
||||
message_info: BaseMessageInfo 对象
|
||||
|
||||
|
||||
Returns:
|
||||
str | None: JSON 字符串格式的 additional_config,如果为空则返回 None
|
||||
"""
|
||||
import orjson
|
||||
|
||||
|
||||
# 首先获取adapter传递的additional_config
|
||||
additional_config_data = {}
|
||||
if hasattr(message_info, 'additional_config') and message_info.additional_config:
|
||||
|
||||
@@ -9,10 +9,10 @@ from maim_message import BaseMessageInfo, MessageBase, Seg, UserInfo
|
||||
from rich.traceback import install
|
||||
|
||||
from src.chat.message_receive.chat_stream import ChatStream
|
||||
from src.chat.utils.self_voice_cache import consume_self_voice_text
|
||||
from src.chat.utils.utils_image import get_image_manager
|
||||
from src.chat.utils.utils_video import get_video_analyzer, is_video_analysis_available
|
||||
from src.chat.utils.utils_voice import get_voice_text
|
||||
from src.chat.utils.self_voice_cache import consume_self_voice_text
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
|
||||
@@ -212,7 +212,7 @@ class MessageRecv(Message):
|
||||
return f"[语音:{cached_text}]"
|
||||
else:
|
||||
logger.warning("机器人自身语音消息缓存未命中,将回退到标准语音识别。")
|
||||
|
||||
|
||||
# 标准语音识别流程 (也作为缓存未命中的后备方案)
|
||||
if isinstance(segment.data, str):
|
||||
return await get_voice_text(segment.data)
|
||||
@@ -239,6 +239,12 @@ class MessageRecv(Message):
|
||||
}
|
||||
"""
|
||||
return ""
|
||||
elif segment.type == "file":
|
||||
if isinstance(segment.data, dict):
|
||||
file_name = segment.data.get('name', '未知文件')
|
||||
file_size = segment.data.get('size', '未知大小')
|
||||
return f"[文件:{file_name} ({file_size}字节)]"
|
||||
return "[收到一个文件]"
|
||||
elif segment.type == "video":
|
||||
self.is_picid = False
|
||||
self.is_emoji = False
|
||||
@@ -296,8 +302,8 @@ class MessageRecv(Message):
|
||||
else:
|
||||
return ""
|
||||
else:
|
||||
logger.info("未启用视频识别")
|
||||
return "[视频]"
|
||||
logger.warning(f"未知的消息段类型: {segment.type}")
|
||||
return f"[{segment.type} 消息]"
|
||||
except Exception as e:
|
||||
logger.error(f"处理消息段失败: {e!s}, 类型: {segment.type}, 数据: {segment.data}")
|
||||
return f"[处理失败的{segment.type}消息]"
|
||||
@@ -364,7 +370,7 @@ class MessageRecvS4U(MessageRecv):
|
||||
self.is_picid = False
|
||||
self.is_emoji = False
|
||||
self.is_voice = True
|
||||
|
||||
|
||||
# 检查消息是否由机器人自己发送
|
||||
# 检查消息是否由机器人自己发送
|
||||
if self.message_info and self.message_info.user_info and str(self.message_info.user_info.user_id) == str(global_config.bot.qq_account):
|
||||
@@ -433,6 +439,12 @@ class MessageRecvS4U(MessageRecv):
|
||||
self.is_screen = True
|
||||
self.screen_info = segment.data
|
||||
return "屏幕信息"
|
||||
elif segment.type == "file":
|
||||
if isinstance(segment.data, dict):
|
||||
file_name = segment.data.get('name', '未知文件')
|
||||
file_size = segment.data.get('size', '未知大小')
|
||||
return f"[文件:{file_name} ({file_size}字节)]"
|
||||
return "[收到一个文件]"
|
||||
elif segment.type == "video":
|
||||
self.is_voice = False
|
||||
self.is_picid = False
|
||||
@@ -490,8 +502,8 @@ class MessageRecvS4U(MessageRecv):
|
||||
else:
|
||||
return ""
|
||||
else:
|
||||
logger.info("未启用视频识别")
|
||||
return "[视频]"
|
||||
logger.warning(f"未知的消息段类型: {segment.type}")
|
||||
return f"[{segment.type} 消息]"
|
||||
except Exception as e:
|
||||
logger.error(f"处理消息段失败: {e!s}, 类型: {segment.type}, 数据: {segment.data}")
|
||||
return f"[处理失败的{segment.type}消息]"
|
||||
|
||||
@@ -796,44 +796,63 @@ class DefaultReplyer:
|
||||
async def build_keywords_reaction_prompt(self, target: str | None) -> str:
|
||||
"""构建关键词反应提示
|
||||
|
||||
该方法根据配置的关键词和正则表达式规则,
|
||||
检查目标消息内容是否触发了任何反应。
|
||||
如果匹配成功,它会生成一个包含所有触发反应的提示字符串,
|
||||
用于指导LLM的回复。
|
||||
|
||||
Args:
|
||||
target: 目标消息内容
|
||||
|
||||
Returns:
|
||||
str: 关键词反应提示字符串
|
||||
str: 关键词反应提示字符串,如果没有触发任何反应则为空字符串
|
||||
"""
|
||||
# 关键词检测与反应
|
||||
keywords_reaction_prompt = ""
|
||||
if target is None:
|
||||
return ""
|
||||
|
||||
reaction_prompt = ""
|
||||
try:
|
||||
# 添加None检查,防止NoneType错误
|
||||
if target is None:
|
||||
return keywords_reaction_prompt
|
||||
current_chat_stream_id_str = self.chat_stream.get_raw_id()
|
||||
# 2. 筛选适用的规则(全局规则 + 特定于当前聊天的规则)
|
||||
applicable_rules = []
|
||||
for rule in global_config.reaction.rules:
|
||||
if rule.chat_stream_id == "" or rule.chat_stream_id == current_chat_stream_id_str:
|
||||
applicable_rules.append(rule) # noqa: PERF401
|
||||
|
||||
# 处理关键词规则
|
||||
for rule in global_config.keyword_reaction.keyword_rules:
|
||||
if any(keyword in target for keyword in rule.keywords):
|
||||
logger.info(f"检测到关键词规则:{rule.keywords},触发反应:{rule.reaction}")
|
||||
keywords_reaction_prompt += f"{rule.reaction},"
|
||||
# 3. 遍历适用规则并执行匹配
|
||||
for rule in applicable_rules:
|
||||
matched = False
|
||||
if rule.rule_type == "keyword":
|
||||
if any(keyword in target for keyword in rule.patterns):
|
||||
logger.info(f"检测到关键词规则:{rule.patterns},触发反应:{rule.reaction}")
|
||||
reaction_prompt += f"{rule.reaction},"
|
||||
matched = True
|
||||
|
||||
elif rule.rule_type == "regex":
|
||||
for pattern_str in rule.patterns:
|
||||
try:
|
||||
pattern = re.compile(pattern_str)
|
||||
if result := pattern.search(target):
|
||||
reaction = rule.reaction
|
||||
# 替换命名捕获组
|
||||
for name, content in result.groupdict().items():
|
||||
reaction = reaction.replace(f"[{name}]", content)
|
||||
logger.info(f"匹配到正则表达式:{pattern_str},触发反应:{reaction}")
|
||||
reaction_prompt += f"{reaction},"
|
||||
matched = True
|
||||
break # 一个正则规则里只要有一个 pattern 匹配成功即可
|
||||
except re.error as e:
|
||||
logger.error(f"正则表达式编译错误: {pattern_str}, 错误信息: {e!s}")
|
||||
continue
|
||||
|
||||
if matched:
|
||||
# 如果需要每条消息只触发一个反应规则,可以在这里 break
|
||||
pass
|
||||
|
||||
# 处理正则表达式规则
|
||||
for rule in global_config.keyword_reaction.regex_rules:
|
||||
for pattern_str in rule.regex:
|
||||
try:
|
||||
pattern = re.compile(pattern_str)
|
||||
if result := pattern.search(target):
|
||||
reaction = rule.reaction
|
||||
for name, content in result.groupdict().items():
|
||||
reaction = reaction.replace(f"[{name}]", content)
|
||||
logger.info(f"匹配到正则表达式:{pattern_str},触发反应:{reaction}")
|
||||
keywords_reaction_prompt += f"{reaction},"
|
||||
break
|
||||
except re.error as e:
|
||||
logger.error(f"正则表达式编译错误: {pattern_str}, 错误信息: {e!s}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error(f"关键词检测与反应时发生异常: {e!s}", exc_info=True)
|
||||
|
||||
return keywords_reaction_prompt
|
||||
return reaction_prompt
|
||||
|
||||
async def build_notice_block(self, chat_id: str) -> str:
|
||||
"""构建notice信息块
|
||||
|
||||
@@ -303,7 +303,7 @@ class Prompt:
|
||||
|
||||
@staticmethod
|
||||
def _process_escaped_braces(template) -> str:
|
||||
"""预处理模板,将 `\{` 和 `\}` 替换为临时标记."""
|
||||
r"""预处理模板,将 `\{` 和 `\}` 替换为临时标记."""
|
||||
if isinstance(template, list):
|
||||
template = "\n".join(str(item) for item in template)
|
||||
elif not isinstance(template, str):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Type
|
||||
|
||||
from src.chat.utils.prompt_params import PromptParameters
|
||||
from src.common.logger import get_logger
|
||||
@@ -21,7 +20,7 @@ class PromptComponentManager:
|
||||
3. 提供一个接口,以便在构建核心Prompt时,能够获取并执行所有相关的组件。
|
||||
"""
|
||||
|
||||
def _get_rules_for(self, target_prompt_name: str) -> list[tuple[InjectionRule, Type[BasePrompt]]]:
|
||||
def _get_rules_for(self, target_prompt_name: str) -> list[tuple[InjectionRule, type[BasePrompt]]]:
|
||||
"""
|
||||
获取指定目标Prompt的所有注入规则及其关联的组件类。
|
||||
|
||||
|
||||
@@ -6,15 +6,14 @@
|
||||
避免不必要的自我语音识别。
|
||||
"""
|
||||
import hashlib
|
||||
from typing import Dict
|
||||
|
||||
# 一个简单的内存缓存,用于将机器人自己发送的语音消息映射到其原始文本。
|
||||
# 键是语音base64内容的SHA256哈希值。
|
||||
_self_voice_cache: Dict[str, str] = {}
|
||||
_self_voice_cache: dict[str, str] = {}
|
||||
|
||||
def get_voice_key(base64_content: str) -> str:
|
||||
"""为语音内容生成一个一致的键。"""
|
||||
return hashlib.sha256(base64_content.encode('utf-8')).hexdigest()
|
||||
return hashlib.sha256(base64_content.encode("utf-8")).hexdigest()
|
||||
|
||||
def register_self_voice(base64_content: str, text: str):
|
||||
"""
|
||||
@@ -39,4 +38,4 @@ def consume_self_voice_text(base64_content: str) -> str | None:
|
||||
str | None: 如果找到,则返回原始文本,否则返回None。
|
||||
"""
|
||||
key = get_voice_key(base64_content)
|
||||
return _self_voice_cache.pop(key, None)
|
||||
return _self_voice_cache.pop(key, None)
|
||||
|
||||
@@ -430,19 +430,13 @@ def process_llm_response(text: str, enable_splitter: bool = True, enable_chinese
|
||||
if global_config.response_splitter.enable and enable_splitter:
|
||||
logger.info(f"回复分割器已启用,模式: {global_config.response_splitter.split_mode}。")
|
||||
|
||||
split_mode = global_config.response_splitter.split_mode
|
||||
|
||||
if split_mode == "llm" and "[SPLIT]" in cleaned_text:
|
||||
if "[SPLIT]" in cleaned_text:
|
||||
logger.debug("检测到 [SPLIT] 标记,使用 LLM 自定义分割。")
|
||||
split_sentences_raw = cleaned_text.split("[SPLIT]")
|
||||
split_sentences = [s.strip() for s in split_sentences_raw if s.strip()]
|
||||
else:
|
||||
if split_mode == "llm":
|
||||
logger.debug("未检测到 [SPLIT] 标记,本次不进行分割。")
|
||||
split_sentences = [cleaned_text]
|
||||
else: # mode == "punctuation"
|
||||
logger.debug("使用基于标点的传统模式进行分割。")
|
||||
split_sentences = split_into_sentences_w_remove_punctuation(cleaned_text)
|
||||
logger.debug("使用基于标点的传统模式进行分割。")
|
||||
split_sentences = split_into_sentences_w_remove_punctuation(cleaned_text)
|
||||
else:
|
||||
logger.debug("回复分割器已禁用。")
|
||||
split_sentences = [cleaned_text]
|
||||
|
||||
@@ -19,10 +19,11 @@ async def get_voice_text(voice_base64: str) -> str:
|
||||
|
||||
# 如果选择本地识别
|
||||
if asr_provider == "local":
|
||||
from src.plugin_system.apis import tool_api
|
||||
import tempfile
|
||||
import base64
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from src.plugin_system.apis import tool_api
|
||||
|
||||
local_asr_tool = tool_api.get_tool_instance("local_asr")
|
||||
if not local_asr_tool:
|
||||
@@ -39,8 +40,8 @@ async def get_voice_text(voice_base64: str) -> str:
|
||||
text = await local_asr_tool.execute(function_args={"audio_path": audio_path})
|
||||
if "失败" in text or "出错" in text or "错误" in text:
|
||||
logger.warning(f"本地语音识别失败: {text}")
|
||||
return f"[语音(本地识别失败)]"
|
||||
|
||||
return "[语音(本地识别失败)]"
|
||||
|
||||
logger.info(f"本地语音识别成功: {text}")
|
||||
return f"[语音] {text}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user