This commit is contained in:
Windpicker-owo
2025-10-27 17:17:11 +08:00
45 changed files with 1391 additions and 156 deletions

View File

@@ -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)
# 检查执行结果是否真正成功

View File

@@ -878,7 +878,7 @@ class MemorySystem:
except Exception as e:
logger.warning(f"检索瞬时记忆失败: {e}", exc_info=True)
# 最终截断
final_memories = final_memories[:effective_limit]

View File

@@ -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,
}
}

View File

@@ -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 {}

View File

@@ -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)

View File

@@ -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:

View File

@@ -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}消息]"

View File

@@ -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信息块

View File

@@ -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):

View File

@@ -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的所有注入规则及其关联的组件类。

View File

@@ -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)

View File

@@ -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]

View File

@@ -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}"