Merge branch 'MaiM-with-u:dev' into dev

This commit is contained in:
infinitycat
2025-05-01 02:41:16 +08:00
committed by GitHub
10 changed files with 562 additions and 367 deletions

View File

@@ -633,13 +633,12 @@ HFC_STYLE_CONFIG = {
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}", "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
}, },
"simple": { "simple": {
"console_format": ( "console_format": ("<level>{time:MM-DD HH:mm}</level> | <light-green>专注聊天 | {message}</light-green>"),
"<level>{time:MM-DD HH:mm}</level> | <light-green>专注聊天</light-green> | <light-green>{message}</light-green>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}", "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
}, },
} }
CONFIRM_STYLE_CONFIG = { CONFIRM_STYLE_CONFIG = {
"console_format": "<RED>{message}</RED>", # noqa: E501 "console_format": "<RED>{message}</RED>", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}", "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}",

View File

@@ -22,7 +22,7 @@ logger = get_logger("config")
# 考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 # 考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
is_test = False is_test = False
mai_version_main = "0.6.3" mai_version_main = "0.6.3"
mai_version_fix = "" mai_version_fix = "fix-1"
if mai_version_fix: if mai_version_fix:
if is_test: if is_test:

View File

@@ -10,6 +10,9 @@ logger = get_logger("mai_state")
# -- 状态相关的可配置参数 (可以从 glocal_config 加载) -- # -- 状态相关的可配置参数 (可以从 glocal_config 加载) --
# The line `enable_unlimited_hfc_chat = False` is setting a configuration parameter that controls
# whether a specific debugging feature is enabled or not. When `enable_unlimited_hfc_chat` is set to
# `False`, it means that the debugging feature for unlimited focused chatting is disabled.
# enable_unlimited_hfc_chat = True # 调试用:无限专注聊天 # enable_unlimited_hfc_chat = True # 调试用:无限专注聊天
enable_unlimited_hfc_chat = False enable_unlimited_hfc_chat = False
prevent_offline_state = True prevent_offline_state = True

View File

@@ -18,7 +18,7 @@ from src.heart_flow.sub_mind import SubMind
# 定义常量 (从 interest.py 移动过来) # 定义常量 (从 interest.py 移动过来)
MAX_INTEREST = 15.0 MAX_INTEREST = 15.0
logger = get_logger("subheartflow") logger = get_logger("sub_heartflow")
PROBABILITY_INCREASE_RATE_PER_SECOND = 0.1 PROBABILITY_INCREASE_RATE_PER_SECOND = 0.1
PROBABILITY_DECREASE_RATE_PER_SECOND = 0.1 PROBABILITY_DECREASE_RATE_PER_SECOND = 0.1
@@ -346,7 +346,7 @@ class SubHeartflow:
return True # 已经在运行 return True # 已经在运行
# 如果实例不存在,则创建并启动 # 如果实例不存在,则创建并启动
logger.info(f"{log_prefix} 麦麦准备开始专注聊天 (创建新实例)...") logger.info(f"{log_prefix} 麦麦准备开始专注聊天...")
try: try:
# 创建 HeartFChatting 实例,并传递 从构造函数传入的 回调函数 # 创建 HeartFChatting 实例,并传递 从构造函数传入的 回调函数
self.heart_fc_instance = HeartFChatting( self.heart_fc_instance = HeartFChatting(
@@ -359,7 +359,7 @@ class SubHeartflow:
# 初始化并启动 HeartFChatting # 初始化并启动 HeartFChatting
if await self.heart_fc_instance._initialize(): if await self.heart_fc_instance._initialize():
await self.heart_fc_instance.start() await self.heart_fc_instance.start()
logger.info(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。") logger.debug(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。")
return True return True
else: else:
logger.error(f"{log_prefix} HeartFChatting 初始化失败,无法进入专注模式。") logger.error(f"{log_prefix} HeartFChatting 初始化失败,无法进入专注模式。")
@@ -397,7 +397,7 @@ class SubHeartflow:
# 移除限额检查逻辑 # 移除限额检查逻辑
logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态") logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态")
if await self._start_heart_fc_chat(): if await self._start_heart_fc_chat():
logger.info(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。") logger.debug(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。")
state_changed = True state_changed = True
else: else:
logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。") logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。")
@@ -511,12 +511,12 @@ class SubHeartflow:
# 取消可能存在的旧后台任务 (self.task) # 取消可能存在的旧后台任务 (self.task)
if self.task and not self.task.done(): if self.task and not self.task.done():
logger.info(f"{self.log_prefix} 取消子心流主任务 (Shutdown)...") logger.debug(f"{self.log_prefix} 取消子心流主任务 (Shutdown)...")
self.task.cancel() self.task.cancel()
try: try:
await asyncio.wait_for(self.task, timeout=1.0) # 给点时间响应取消 await asyncio.wait_for(self.task, timeout=1.0) # 给点时间响应取消
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info(f"{self.log_prefix} 子心流主任务已取消 (Shutdown)。") logger.debug(f"{self.log_prefix} 子心流主任务已取消 (Shutdown)。")
except asyncio.TimeoutError: except asyncio.TimeoutError:
logger.warning(f"{self.log_prefix} 等待子心流主任务取消超时 (Shutdown)。") logger.warning(f"{self.log_prefix} 等待子心流主任务取消超时 (Shutdown)。")
except Exception as e: except Exception as e:

View File

@@ -34,9 +34,12 @@ MAX_EMOJI_FOR_PROMPT = 20 # 最大允许的表情包描述数量于图片替换
class MaiEmoji: class MaiEmoji:
"""定义一个表情包""" """定义一个表情包"""
def __init__(self, filename: str, path: str): def __init__(self, full_path: str):
self.path = path # 存储目录路径 if not full_path:
self.filename = filename raise ValueError("full_path cannot be empty")
self.full_path = full_path # 文件的完整路径 (包括文件名)
self.path = os.path.dirname(full_path) # 文件所在的目录路径
self.filename = os.path.basename(full_path) # 文件名
self.embedding = [] self.embedding = []
self.hash = "" # 初始为空,在创建实例时会计算 self.hash = "" # 初始为空,在创建实例时会计算
self.description = "" self.description = ""
@@ -48,35 +51,58 @@ class MaiEmoji:
self.format = "" self.format = ""
async def initialize_hash_format(self): async def initialize_hash_format(self):
"""从文件创建表情包实例 """从文件创建表情包实例, 计算哈希值和格式"""
image_base64 = None
参数: image_bytes = None
file_path: 文件的完整路径
返回:
MaiEmoji: 创建的表情包实例如果失败则返回None
"""
try: try:
file_path = os.path.join(self.path, self.filename) # 使用 full_path 检查文件是否存在
if not os.path.exists(file_path): if not os.path.exists(self.full_path):
logger.error(f"[错误] 表情包文件不存在: {file_path}") logger.error(f"[初始化错误] 表情包文件不存在: {self.full_path}")
self.is_deleted = True
return None return None
image_base64 = image_path_to_base64(file_path) # 使用 full_path 读取文件
logger.debug(f"[初始化] 正在读取文件: {self.full_path}")
image_base64 = image_path_to_base64(self.full_path)
if image_base64 is None: if image_base64 is None:
logger.error(f"[错误] 无法读取图片: {file_path}") logger.error(f"[初始化错误] 无法读取或转换Base64: {self.full_path}")
self.is_deleted = True
return None return None
logger.debug(f"[初始化] 文件读取成功 (Base64预览: {image_base64[:50]}...)")
# 计算哈希值 # 计算哈希值
logger.debug(f"[初始化] 正在解码Base64并计算哈希: {self.filename}")
image_bytes = base64.b64decode(image_base64) image_bytes = base64.b64decode(image_base64)
self.hash = hashlib.md5(image_bytes).hexdigest() self.hash = hashlib.md5(image_bytes).hexdigest()
logger.debug(f"[初始化] 哈希计算成功: {self.hash}")
# 获取图片格式 # 获取图片格式
self.format = Image.open(io.BytesIO(image_bytes)).format.lower() logger.debug(f"[初始化] 正在使用Pillow获取格式: {self.filename}")
try:
with Image.open(io.BytesIO(image_bytes)) as img:
self.format = img.format.lower()
logger.debug(f"[初始化] 格式获取成功: {self.format}")
except Exception as pil_error:
logger.error(f"[初始化错误] Pillow无法处理图片 ({self.filename}): {pil_error}")
logger.error(traceback.format_exc())
self.is_deleted = True
return None
# 如果所有步骤成功,返回 True
return True
except FileNotFoundError:
logger.error(f"[初始化错误] 文件在处理过程中丢失: {self.full_path}")
self.is_deleted = True
return None
except base64.binascii.Error as b64_error:
logger.error(f"[初始化错误] Base64解码失败 ({self.filename}): {b64_error}")
self.is_deleted = True
return None
except Exception as e: except Exception as e:
logger.error(f"[错误] 初始化表情包失败: {str(e)}") logger.error(f"[初始化错误] 初始化表情包时发生未预期错误 ({self.filename}): {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
self.is_deleted = True
return None return None
async def register_to_db(self): async def register_to_db(self):
@@ -87,44 +113,47 @@ class MaiEmoji:
""" """
try: try:
# 确保目标目录存在 # 确保目标目录存在
os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True)
# 源路径是当前实例的完整路径 # 源路径是当前实例的完整路径 self.full_path
source_path = os.path.join(self.path, self.filename) source_full_path = self.full_path
# 目标路径 # 目标完整路径
destination_path = os.path.join(EMOJI_REGISTED_DIR, self.filename) destination_full_path = os.path.join(EMOJI_REGISTED_DIR, self.filename)
# 检查源文件是否存在 # 检查源文件是否存在
if not os.path.exists(source_path): if not os.path.exists(source_full_path):
logger.error(f"[错误] 源文件不存在: {source_path}") logger.error(f"[错误] 源文件不存在: {source_full_path}")
return False return False
# --- 文件移动 --- # --- 文件移动 ---
try: try:
# 如果目标文件已存在,先删除 (确保移动成功) # 如果目标文件已存在,先删除 (确保移动成功)
if os.path.exists(destination_path): if os.path.exists(destination_full_path):
os.remove(destination_path) os.remove(destination_full_path)
os.rename(source_path, destination_path) os.rename(source_full_path, destination_full_path)
logger.debug(f"[移动] 文件从 {source_path} 移动到 {destination_path}") logger.debug(f"[移动] 文件从 {source_full_path} 移动到 {destination_full_path}")
# 更新实例的路径属性为新目录 # 更新实例的路径属性为新路径
self.full_path = destination_full_path
self.path = EMOJI_REGISTED_DIR self.path = EMOJI_REGISTED_DIR
# self.filename 保持不变
except Exception as move_error: except Exception as move_error:
logger.error(f"[错误] 移动文件失败: {str(move_error)}") logger.error(f"[错误] 移动文件失败: {str(move_error)}")
return False # 文件移动失败,不继续 # 如果移动失败,尝试将实例状态恢复?暂时不处理,仅返回失败
return False
# --- 数据库操作 --- # --- 数据库操作 ---
try: try:
# 准备数据库记录 for emoji collection # 准备数据库记录 for emoji collection
emoji_record = { emoji_record = {
"filename": self.filename, "filename": self.filename,
"path": os.path.join(self.path, self.filename), # 使用更新后的路径 "path": self.path, # 存储目录路径
"full_path": self.full_path, # 存储完整文件路径
"embedding": self.embedding, "embedding": self.embedding,
"description": self.description, "description": self.description,
"emotion": self.emotion, # 添加情感标签字段 "emotion": self.emotion,
"hash": self.hash, "hash": self.hash,
"format": self.format, "format": self.format,
"timestamp": int(self.register_time), # 使用实例的注册时间 "timestamp": int(self.register_time),
"usage_count": self.usage_count, "usage_count": self.usage_count,
"last_used_time": self.last_used_time, "last_used_time": self.last_used_time,
} }
@@ -132,17 +161,24 @@ class MaiEmoji:
# 使用upsert确保记录存在或被更新 # 使用upsert确保记录存在或被更新
db["emoji"].update_one({"hash": self.hash}, {"$set": emoji_record}, upsert=True) db["emoji"].update_one({"hash": self.hash}, {"$set": emoji_record}, upsert=True)
logger.success(f"[注册] 表情包信息保存到数据库: {self.emotion}") logger.success(f"[注册] 表情包信息保存到数据库: {self.filename} ({self.emotion})")
return True return True
except Exception as db_error: except Exception as db_error:
logger.error(f"[错误] 保存数据库失败: {str(db_error)}") logger.error(f"[错误] 保存数据库失败 ({self.filename}): {str(db_error)}")
# 考虑是否需要将文件移回?为了简化,暂时只记录错误 # 数据库保存失败,是否需要将文件移回?为了简化,暂时只记录错误
# 可以考虑在这里尝试删除已移动的文件,避免残留
try:
if os.path.exists(self.full_path): # full_path 此时是目标路径
os.remove(self.full_path)
logger.warning(f"[回滚] 已删除移动失败后残留的文件: {self.full_path}")
except Exception as remove_error:
logger.error(f"[错误] 回滚删除文件失败: {remove_error}")
return False return False
except Exception as e: except Exception as e:
logger.error(f"[错误] 注册表情包失败: {str(e)}") logger.error(f"[错误] 注册表情包失败 ({self.filename}): {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return False return False
@@ -156,30 +192,36 @@ class MaiEmoji:
""" """
try: try:
# 1. 删除文件 # 1. 删除文件
if os.path.exists(os.path.join(self.path, self.filename)): file_to_delete = self.full_path
if os.path.exists(file_to_delete):
try: try:
os.remove(os.path.join(self.path, self.filename)) os.remove(file_to_delete)
logger.debug(f"[删除] 文件: {os.path.join(self.path, self.filename)}") logger.debug(f"[删除] 文件: {file_to_delete}")
except Exception as e: except Exception as e:
logger.error(f"[错误] 删除文件失败 {os.path.join(self.path, self.filename)}: {str(e)}") logger.error(f"[错误] 删除文件失败 {file_to_delete}: {str(e)}")
# 继续执行,即使文件删除失败尝试删除数据库记录 # 文件删除失败,但仍然尝试删除数据库记录
# 2. 删除数据库记录 # 2. 删除数据库记录
result = db.emoji.delete_one({"hash": self.hash}) result = db.emoji.delete_one({"hash": self.hash})
deleted_in_db = result.deleted_count > 0 deleted_in_db = result.deleted_count > 0
if deleted_in_db: if deleted_in_db:
logger.info(f"[删除] 表情包 {self.filename} 无对应文件,已删除") logger.info(f"[删除] 表情包数据库记录 {self.filename} (Hash: {self.hash})")
# 3. 标记对象已被删除 # 3. 标记对象已被删除
self.is_deleted = True self.is_deleted = True
return True return True
else: else:
logger.error(f"[错误] 删除表情包记录失败: {self.hash}") # 如果数据库记录删除失败,但文件可能已删除,记录一个警告
if not os.path.exists(file_to_delete):
logger.warning(
f"[警告] 表情包文件 {file_to_delete} 已删除,但数据库记录删除失败 (Hash: {self.hash})"
)
else:
logger.error(f"[错误] 删除表情包数据库记录失败: {self.hash}")
return False return False
except Exception as e: except Exception as e:
logger.error(f"[错误] 删除表情包失败: {str(e)}") logger.error(f"[错误] 删除表情包失败 ({self.filename}): {str(e)}")
return False return False
@@ -209,6 +251,7 @@ class EmojiManager:
def _ensure_emoji_dir(self): def _ensure_emoji_dir(self):
"""确保表情存储目录存在""" """确保表情存储目录存在"""
os.makedirs(EMOJI_DIR, exist_ok=True) os.makedirs(EMOJI_DIR, exist_ok=True)
os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True)
def initialize(self): def initialize(self):
"""初始化数据库连接和表情目录""" """初始化数据库连接和表情目录"""
@@ -265,22 +308,27 @@ class EmojiManager:
Args: Args:
text_emotion: 输入的情感描述文本 text_emotion: 输入的情感描述文本
Returns: Returns:
Optional[Tuple[str, str]]: (表情包文件路径, 表情包描述)如果没有找到则返回None Optional[Tuple[str, str]]: (表情包完整文件路径, 表情包描述)如果没有找到则返回None
""" """
try: try:
self._ensure_db() self._ensure_db()
_time_start = time.time() _time_start = time.time()
# 获取所有表情包 # 获取所有表情包 (从内存缓存中获取)
all_emojis = self.emoji_objects all_emojis = self.emoji_objects
if not all_emojis: if not all_emojis:
logger.warning("数据库中没有任何表情包") logger.warning("内存中没有任何表情包对象")
# 可以考虑再查一次数据库?或者依赖定期任务更新
return None return None
# 计算每个表情包与输入文本的最大情感相似度 # 计算每个表情包与输入文本的最大情感相似度
emoji_similarities = [] emoji_similarities = []
for emoji in all_emojis: for emoji in all_emojis:
# 跳过已标记为删除的对象
if emoji.is_deleted:
continue
emotions = emoji.emotion emotions = emoji.emotion
if not emotions: if not emotions:
continue continue
@@ -321,9 +369,10 @@ class EmojiManager:
_time_end = time.time() _time_end = time.time()
logger.info( # 使用匹配到的 emotion 记录日志喵~ logger.info( # 使用匹配到的 emotion 记录日志喵~
f"为[{text_emotion}]找到表情包: {matched_emotion},({similarity:.4f})" f"为[{text_emotion}]找到表情包: {matched_emotion} ({selected_emoji.filename}), Similarity: {similarity:.4f}"
) )
return selected_emoji.path, f"[ {selected_emoji.description} ]" # 返回完整文件路径和描述
return selected_emoji.full_path, f"[ {selected_emoji.description} ]"
except Exception as e: except Exception as e:
logger.error(f"[错误] 获取表情包失败: {str(e)}") logger.error(f"[错误] 获取表情包失败: {str(e)}")
@@ -371,40 +420,50 @@ class EmojiManager:
self.emoji_num = total_count self.emoji_num = total_count
removed_count = 0 removed_count = 0
# 使用列表复制进行遍历,因为我们会在遍历过程中修改列表 # 使用列表复制进行遍历,因为我们会在遍历过程中修改列表
for emoji in self.emoji_objects[:]: objects_to_remove = []
for emoji in self.emoji_objects:
try: try:
# 跳过已经标记为删除的,避免重复处理
if emoji.is_deleted:
objects_to_remove.append(emoji) # 收集起来一次性移除
continue
# 检查文件是否存在 # 检查文件是否存在
if not os.path.exists(emoji.path): if not os.path.exists(emoji.full_path):
logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}") logger.warning(f"[检查] 表情包文件丢失: {emoji.full_path}")
# 执行表情包对象的删除方法 # 执行表情包对象的删除方法
await emoji.delete() await emoji.delete() # delete 方法现在会标记 is_deleted
# 从列表中移除该对象 objects_to_remove.append(emoji) # 标记删除后,也收集起来移除
self.emoji_objects.remove(emoji)
# 更新计数 # 更新计数
self.emoji_num -= 1 self.emoji_num -= 1
removed_count += 1 removed_count += 1
continue continue
if emoji.description == None: # 检查描述是否为空 (如果为空也视为无效)
logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}") if not emoji.description:
# 执行表情包对象的删除方法 logger.warning(f"[检查] 表情包描述为空,视为无效: {emoji.filename}")
await emoji.delete() await emoji.delete()
# 从列表中移除该对象 objects_to_remove.append(emoji)
self.emoji_objects.remove(emoji)
# 更新计数
self.emoji_num -= 1 self.emoji_num -= 1
removed_count += 1 removed_count += 1
continue continue
except Exception as item_error: except Exception as item_error:
logger.error(f"[错误] 处理表情包记录时出错: {str(item_error)}") logger.error(f"[错误] 处理表情包记录时出错 ({emoji.filename}): {str(item_error)}")
# 即使出错,也尝试继续检查下一个
continue continue
# 从 self.emoji_objects 中移除标记的对象
if objects_to_remove:
self.emoji_objects = [e for e in self.emoji_objects if e not in objects_to_remove]
# 清理 EMOJI_REGISTED_DIR 目录中未被追踪的文件
await self.clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects) await self.clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects)
# 输出清理结果 # 输出清理结果
if removed_count > 0: if removed_count > 0:
logger.success(f"[清理] 已清理 {removed_count} 个失效的表情包记录") logger.success(f"[清理] 已清理 {removed_count} 个失效/文件丢失的表情包记录")
logger.info(f"[统计] 清理前: {total_count} | 清理后: {len(self.emoji_objects)}") logger.info(f"[统计] 清理前记录数: {total_count} | 清理后有效记录数: {len(self.emoji_objects)}")
else: else:
logger.info(f"[检查] 已检查 {total_count} 个表情包记录,全部完好") logger.info(f"[检查] 已检查 {total_count} 个表情包记录,全部完好")
@@ -467,45 +526,72 @@ class EmojiManager:
await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60) await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
async def get_all_emoji_from_db(self): async def get_all_emoji_from_db(self):
"""获取所有表情包并初始化为MaiEmoji类对象 """获取所有表情包并初始化为MaiEmoji类对象,更新 self.emoji_objects"""
参数:
hash: 可选,如果提供则只返回指定哈希值的表情包
返回:
list[MaiEmoji]: 表情包对象列表
"""
try: try:
self._ensure_db() self._ensure_db()
logger.info("[数据库] 开始加载所有表情包记录...")
# 获取所有表情包
all_emoji_data = list(db.emoji.find()) all_emoji_data = list(db.emoji.find())
# 将数据库记录转换为MaiEmoji对象
emoji_objects = [] emoji_objects = []
load_errors = 0
for emoji_data in all_emoji_data: for emoji_data in all_emoji_data:
emoji = MaiEmoji( full_path = emoji_data.get("full_path")
filename=emoji_data.get("filename", ""), if not full_path:
path=emoji_data.get("path", ""), logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}")
) load_errors += 1
continue # 跳过缺少 full_path 的记录
# 设置额外属性 try:
emoji.hash = emoji_data.get("hash", "") # 使用 full_path 初始化 MaiEmoji 对象
emoji.usage_count = emoji_data.get("usage_count", 0) emoji = MaiEmoji(full_path=full_path)
emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time()))
emoji.register_time = emoji_data.get("timestamp", time.time())
emoji.description = emoji_data.get("description", "")
emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载
emoji_objects.append(emoji)
# 存储到EmojiManager中 # 设置从数据库加载的属性
emoji.hash = emoji_data.get("hash", "")
# 如果 hash 为空,也跳过?取决于业务逻辑
if not emoji.hash:
logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}")
load_errors += 1
continue
emoji.description = emoji_data.get("description", "")
emoji.emotion = emoji_data.get("emotion", [])
emoji.usage_count = emoji_data.get("usage_count", 0)
# 优先使用 last_used_time否则用 timestamp最后用当前时间
last_used = emoji_data.get("last_used_time")
timestamp = emoji_data.get("timestamp")
emoji.last_used_time = (
last_used if last_used is not None else (timestamp if timestamp is not None else time.time())
)
emoji.register_time = timestamp if timestamp is not None else time.time()
emoji.format = emoji_data.get("format", "") # 加载格式
# 不需要再手动设置 path 和 filename__init__ 会自动处理
emoji_objects.append(emoji)
except ValueError as ve: # 捕获 __init__ 可能的错误
logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}")
load_errors += 1
except Exception as e:
logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}")
load_errors += 1
# 更新内存中的列表和数量
self.emoji_objects = emoji_objects self.emoji_objects = emoji_objects
self.emoji_num = len(emoji_objects)
logger.success(f"[数据库] 加载完成: 共加载 {self.emoji_num} 个表情包记录。")
if load_errors > 0:
logger.warning(f"[数据库] 加载过程中出现 {load_errors} 个错误。")
except Exception as e: except Exception as e:
logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}") logger.error(f"[错误] 从数据库加载所有表情包对象失败: {str(e)}")
self.emoji_objects = [] # 加载失败则清空列表
self.emoji_num = 0
async def get_emoji_from_db(self, hash=None): async def get_emoji_from_db(self, hash=None):
"""获取所有表情包并初始化为MaiEmoji类对象 """获取指定哈希值的表情包并初始化为MaiEmoji类对象列表 (主要用于调试或特定查找)
参数: 参数:
hash: 可选,如果提供则只返回指定哈希值的表情包 hash: 可选,如果提供则只返回指定哈希值的表情包
@@ -516,50 +602,73 @@ class EmojiManager:
try: try:
self._ensure_db() self._ensure_db()
# 准备查询条件
query = {} query = {}
if hash: if hash:
query = {"hash": hash} query = {"hash": hash}
else:
# 获取所有表情包 logger.warning(
all_emoji_data = list(db.emoji.find(query)) "[查询] 未提供 hash将尝试加载所有表情包建议使用 get_all_emoji_from_db 更新管理器状态。"
# 将数据库记录转换为MaiEmoji对象
emoji_objects = []
for emoji_data in all_emoji_data:
emoji = MaiEmoji(
filename=emoji_data.get("filename", ""),
path=emoji_data.get("path", ""),
) )
# 设置额外属性 emoji_data_list = list(db.emoji.find(query))
emoji.usage_count = emoji_data.get("usage_count", 0) emoji_objects = []
emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time())) load_errors = 0
emoji.register_time = emoji_data.get("timestamp", time.time())
emoji.description = emoji_data.get("description", "")
emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载
emoji_objects.append(emoji) for emoji_data in emoji_data_list:
full_path = emoji_data.get("full_path")
if not full_path:
logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}")
load_errors += 1
continue
# 存储到EmojiManager中 try:
self.emoji_objects = emoji_objects emoji = MaiEmoji(full_path=full_path)
emoji.hash = emoji_data.get("hash", "")
if not emoji.hash:
logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}")
load_errors += 1
continue
emoji.description = emoji_data.get("description", "")
emoji.emotion = emoji_data.get("emotion", [])
emoji.usage_count = emoji_data.get("usage_count", 0)
last_used = emoji_data.get("last_used_time")
timestamp = emoji_data.get("timestamp")
emoji.last_used_time = (
last_used if last_used is not None else (timestamp if timestamp is not None else time.time())
)
emoji.register_time = timestamp if timestamp is not None else time.time()
emoji.format = emoji_data.get("format", "")
emoji_objects.append(emoji)
except ValueError as ve:
logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}")
load_errors += 1
except Exception as e:
logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}")
load_errors += 1
if load_errors > 0:
logger.warning(f"[查询] 加载过程中出现 {load_errors} 个错误。")
return emoji_objects return emoji_objects
except Exception as e: except Exception as e:
logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}") logger.error(f"[错误] 从数据库获取表情包对象失败: {str(e)}")
return [] return []
async def get_emoji_from_manager(self, hash) -> MaiEmoji: async def get_emoji_from_manager(self, hash) -> Optional[MaiEmoji]:
"""EmojiManager中获取表情包 """内存中的 emoji_objects 列表获取表情包
参数: 参数:
hash:如果提供则只返回指定哈希值的表情包 hash: 要查找的表情包哈希值
返回:
MaiEmoji 或 None: 如果找到则返回 MaiEmoji 对象,否则返回 None
""" """
for emoji in self.emoji_objects: for emoji in self.emoji_objects:
if emoji.hash == hash: # 确保对象未被标记为删除且哈希值匹配
if not emoji.is_deleted and emoji.hash == hash:
return emoji return emoji
return None return None # 如果循环结束还没找到,则返回 None
async def delete_emoji(self, emoji_hash: str) -> bool: async def delete_emoji(self, emoji_hash: str) -> bool:
"""根据哈希值删除表情包 """根据哈希值删除表情包
@@ -779,51 +888,111 @@ class EmojiManager:
Returns: Returns:
bool: 注册是否成功 bool: 注册是否成功
""" """
file_full_path = os.path.join(EMOJI_DIR, filename)
if not os.path.exists(file_full_path):
logger.error(f"[注册失败] 文件不存在: {file_full_path}")
return False
try: try:
# 使用MaiEmoji类创建表情包实例 # 1. 创建 MaiEmoji 实例并初始化哈希和格式
new_emoji = MaiEmoji(filename, EMOJI_DIR) new_emoji = MaiEmoji(full_path=file_full_path)
await new_emoji.initialize_hash_format() init_result = await new_emoji.initialize_hash_format()
emoji_base64 = image_path_to_base64(os.path.join(EMOJI_DIR, filename)) if init_result is None or new_emoji.is_deleted: # 初始化失败或文件读取错误
description, emotions = await self.build_emoji_description(emoji_base64) logger.error(f"[注册失败] 初始化哈希和格式失败: {filename}")
if description == "" or description == None: # 是否需要删除源文件?看业务需求,暂时不删
return False return False
new_emoji.description = description
new_emoji.emotion = emotions
# 检查是否已经注册过 # 2. 检查哈希是否已存在 (在内存中检查)
# 对比内存中是否存在相同哈希值的表情包
if await self.get_emoji_from_manager(new_emoji.hash): if await self.get_emoji_from_manager(new_emoji.hash):
logger.warning(f"[警告] 表情包已存在: {filename}") logger.warning(f"[注册跳过] 表情包已存在 (Hash: {new_emoji.hash}): {filename}")
# 删除重复的源文件
try:
os.remove(file_full_path)
logger.info(f"[清理] 删除重复的待注册文件: {filename}")
except Exception as e:
logger.error(f"[错误] 删除重复文件失败: {str(e)}")
return False # 返回 False 表示未注册新表情
# 3. 构建描述和情感
try:
emoji_base64 = image_path_to_base64(file_full_path)
if emoji_base64 is None: # 再次检查读取
logger.error(f"[注册失败] 无法读取图片以生成描述: {filename}")
return False
description, emotions = await self.build_emoji_description(emoji_base64)
if not description: # 检查描述是否成功生成或审核通过
logger.warning(f"[注册失败] 未能生成有效描述或审核未通过: {filename}")
# 删除未能生成描述的文件
try:
os.remove(file_full_path)
logger.info(f"[清理] 删除描述生成失败的文件: {filename}")
except Exception as e:
logger.error(f"[错误] 删除描述生成失败文件时出错: {str(e)}")
return False
new_emoji.description = description
new_emoji.emotion = emotions
except Exception as build_desc_error:
logger.error(f"[注册失败] 生成描述/情感时出错 ({filename}): {build_desc_error}")
# 同样考虑删除文件
try:
os.remove(file_full_path)
logger.info(f"[清理] 删除描述生成异常的文件: {filename}")
except Exception as e:
logger.error(f"[错误] 删除描述生成异常文件时出错: {str(e)}")
return False return False
# 4. 检查容量并决定是否替换或直接注册
if self.emoji_num >= self.emoji_num_max: if self.emoji_num >= self.emoji_num_max:
logger.warning(f"表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max})") logger.warning(f"表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max}),尝试替换...")
replaced = await self.replace_a_emoji(new_emoji) replaced = await self.replace_a_emoji(new_emoji)
if not replaced: if not replaced:
logger.error("[错误] 替换表情包失败,无法完成注册") logger.error("[注册失败] 替换表情包失败,无法完成注册")
# 替换失败,删除新表情包文件
try:
os.remove(file_full_path) # new_emoji 的 full_path 此时还是源路径
logger.info(f"[清理] 删除替换失败的新表情文件: {filename}")
except Exception as e:
logger.error(f"[错误] 删除替换失败文件时出错: {str(e)}")
return False return False
# 替换成功时replace_a_emoji 内部已处理 new_emoji 的注册和添加到列表
return True return True
else: else:
# 修复:等待异步注册完成 # 直接注册
register_success = await new_emoji.register_to_db() register_success = await new_emoji.register_to_db() # 此方法会移动文件并更新 DB
if register_success: if register_success:
# 注册成功后,添加到内存列表
self.emoji_objects.append(new_emoji) self.emoji_objects.append(new_emoji)
self.emoji_num += 1 self.emoji_num += 1
logger.success(f"[成功] 注册: {filename}") logger.success(f"[成功] 注册新表情包: {filename} (当前: {self.emoji_num}/{self.emoji_num_max})")
return True return True
else: else:
logger.error(f"[错误] 注册表情包到数据库失败: {filename}") logger.error(f"[注册失败] 保存表情包到数据库/移动文件失败: {filename}")
# register_to_db 失败时,内部会尝试清理移动后的文件,源文件可能还在
# 是否需要删除源文件?
if os.path.exists(file_full_path):
try:
os.remove(file_full_path)
logger.info(f"[清理] 删除注册失败的源文件: {filename}")
except Exception as e:
logger.error(f"[错误] 删除注册失败源文件时出错: {str(e)}")
return False return False
except Exception as e: except Exception as e:
logger.error(f"[错误] 注册表情包失败: {str(e)}") logger.error(f"[错误] 注册表情包时发生未预期错误 ({filename}): {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
# 尝试删除源文件以避免循环处理
if os.path.exists(file_full_path):
try:
os.remove(file_full_path)
logger.info(f"[清理] 删除处理异常的源文件: {filename}")
except Exception as remove_error:
logger.error(f"[错误] 删除异常处理文件时出错: {remove_error}")
return False return False
async def clear_temp_emoji(self): async def clear_temp_emoji(self):
"""每天清理临时表情包 """清理临时表情包
清理/data/emoji和/data/image目录下的所有文件 清理/data/emoji和/data/image目录下的所有文件
当目录中文件数超过50时会全部删除 当目录中文件数超过100时会全部删除
""" """
logger.info("[清理] 开始清理缓存...") logger.info("[清理] 开始清理缓存...")
@@ -833,7 +1002,7 @@ class EmojiManager:
if os.path.exists(emoji_dir): if os.path.exists(emoji_dir):
files = os.listdir(emoji_dir) files = os.listdir(emoji_dir)
# 如果文件数超过50就全部删除 # 如果文件数超过50就全部删除
if len(files) > 50: if len(files) > 100:
for filename in files: for filename in files:
file_path = os.path.join(emoji_dir, filename) file_path = os.path.join(emoji_dir, filename)
if os.path.isfile(file_path): if os.path.isfile(file_path):
@@ -845,7 +1014,7 @@ class EmojiManager:
if os.path.exists(image_dir): if os.path.exists(image_dir):
files = os.listdir(image_dir) files = os.listdir(image_dir)
# 如果文件数超过50就全部删除 # 如果文件数超过50就全部删除
if len(files) > 50: if len(files) > 100:
for filename in files: for filename in files:
file_path = os.path.join(image_dir, filename) file_path = os.path.join(image_dir, filename)
if os.path.isfile(file_path): if os.path.isfile(file_path):
@@ -855,29 +1024,40 @@ class EmojiManager:
logger.success("[清理] 完成") logger.success("[清理] 完成")
async def clean_unused_emojis(self, emoji_dir, emoji_objects): async def clean_unused_emojis(self, emoji_dir, emoji_objects):
"""清理未使用的表情包文件 """清理指定目录中未被 emoji_objects 追踪的表情包文件"""
遍历指定文件夹中的所有文件删除未在emoji_objects列表中的文件
"""
# 首先检查目录是否存在喵~
if not os.path.exists(emoji_dir): if not os.path.exists(emoji_dir):
logger.warning(f"[清理] 表情包目录不存在,跳过清理: {emoji_dir}") logger.warning(f"[清理] 目标目录不存在,跳过清理: {emoji_dir}")
return return
# 获取所有表情包路径 try:
emoji_paths = {emoji.path for emoji in emoji_objects} # 获取内存中所有有效表情包的完整路径集合
tracked_full_paths = {emoji.full_path for emoji in emoji_objects if not emoji.is_deleted}
cleaned_count = 0
# 遍历文件夹中的所有文件 # 遍历指定目录中的所有文件
for file_name in os.listdir(emoji_dir): for file_name in os.listdir(emoji_dir):
file_path = os.path.join(emoji_dir, file_name) file_full_path = os.path.join(emoji_dir, file_name)
# 检查文件是否在表情包路径列表中 # 确保处理的是文件而不是子目录
if file_path not in emoji_paths: if not os.path.isfile(file_full_path):
try: continue
# 删除未在表情包列表中的文件
os.remove(file_path) # 如果文件不在被追踪的集合中,则删除
logger.info(f"[清理] 删除未使用的表情包文件: {file_path}") if file_full_path not in tracked_full_paths:
except Exception as e: try:
logger.error(f"[错误] 删除文件时出错: {str(e)}") os.remove(file_full_path)
logger.info(f"[清理] 删除未追踪的表情包文件: {file_full_path}")
cleaned_count += 1
except Exception as e:
logger.error(f"[错误] 删除文件时出错 ({file_full_path}): {str(e)}")
if cleaned_count > 0:
logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个破损表情包。")
else:
logger.info(f"[清理] 目录 {emoji_dir} 中没有需要清理的。")
except Exception as e:
logger.error(f"[错误] 清理未使用表情包文件时出错 ({emoji_dir}): {str(e)}")
# 创建全局单例 # 创建全局单例

View File

@@ -2,6 +2,7 @@ import asyncio
import time import time
import traceback import traceback
import random # <--- 添加导入 import random # <--- 添加导入
import json # <--- 确保导入 json
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
from collections import deque from collections import deque
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
@@ -14,9 +15,7 @@ from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config from src.config.config import global_config
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
from src.plugins.utils.timer_calculator import Timer # <--- Import Timer from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
from src.do_tool.tool_use import ToolUser
from src.plugins.emoji_system.emoji_manager import emoji_manager from src.plugins.emoji_system.emoji_manager import emoji_manager
from src.plugins.utils.json_utils import process_llm_tool_calls, extract_tool_call_arguments
from src.heart_flow.sub_mind import SubMind from src.heart_flow.sub_mind import SubMind
from src.heart_flow.observation import Observation from src.heart_flow.observation import Observation
from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
@@ -37,7 +36,7 @@ EMOJI_SEND_PRO = 0.3 # 设置一个概率,比如 30% 才真的发
CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值 CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值
logger = get_logger("HFC") # Logger Name Changed logger = get_logger("hfc") # Logger Name Changed
# 默认动作定义 # 默认动作定义
@@ -119,35 +118,6 @@ class ActionManager:
"""重置为默认动作集""" """重置为默认动作集"""
self._available_actions = DEFAULT_ACTIONS.copy() self._available_actions = DEFAULT_ACTIONS.copy()
def get_planner_tool_definition(self) -> List[Dict[str, Any]]:
"""获取当前动作集对应的规划器工具定义"""
return [
{
"type": "function",
"function": {
"name": "decide_reply_action",
"description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": list(self._available_actions.keys()),
"description": "决定采取的行动:"
+ ", ".join([f"'{k}'({v})" for k, v in self._available_actions.items()]),
},
"reasoning": {"type": "string", "description": "做出此决定的简要理由。"},
"emoji_query": {
"type": "string",
"description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。",
},
},
"required": ["action", "reasoning"],
},
},
}
]
# 在文件开头添加自定义异常类 # 在文件开头添加自定义异常类
class HeartFCError(Exception): class HeartFCError(Exception):
@@ -222,7 +192,6 @@ class HeartFChatting:
max_tokens=256, max_tokens=256,
request_type="response_heartflow", request_type="response_heartflow",
) )
self.tool_user = ToolUser()
self.heart_fc_sender = HeartFCSender() self.heart_fc_sender = HeartFCSender()
# LLM规划器配置 # LLM规划器配置
@@ -261,7 +230,7 @@ class HeartFChatting:
self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]"
self._initialized = True self._initialized = True
logger.info(f"麦麦感觉到了,可以开始认真水群{self.log_prefix} ") logger.debug(f"{self.log_prefix}麦麦感觉到了,可以开始认真水群 ")
return True return True
async def start(self): async def start(self):
@@ -292,7 +261,7 @@ class HeartFChatting:
pass # 忽略取消或超时错误 pass # 忽略取消或超时错误
self._loop_task = None # 清理旧任务引用 self._loop_task = None # 清理旧任务引用
logger.info(f"{self.log_prefix} 启动认真水群(HFC)主循环...") logger.debug(f"{self.log_prefix} 启动认真水群(HFC)主循环...")
# 创建新的循环任务 # 创建新的循环任务
self._loop_task = asyncio.create_task(self._hfc_loop()) self._loop_task = asyncio.create_task(self._hfc_loop())
# 添加完成回调 # 添加完成回调
@@ -470,6 +439,16 @@ class HeartFChatting:
# execute:执行 # execute:执行
# 在此处添加日志记录
if action == "text_reply":
action_str = "回复"
elif action == "emoji_reply":
action_str = "回复表情"
else:
action_str = "不回复"
logger.info(f"{self.log_prefix} 麦麦决定'{action_str}', 原因'{reasoning}'")
return await self._handle_action( return await self._handle_action(
action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time
) )
@@ -784,41 +763,36 @@ class HeartFChatting:
async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]: async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]:
""" """
规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。 规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。
重构为让LLM返回结构化JSON文本然后在代码中解析。
参数: 参数:
current_mind: 子思维的当前思考结果 current_mind: 子思维的当前思考结果
cycle_timers: 计时器字典 cycle_timers: 计时器字典
is_re_planned: 是否为重新规划 is_re_planned: 是否为重新规划 (此重构中暂时简化,不处理 is_re_planned 的特殊逻辑)
""" """
logger.info(f"{self.log_prefix}[Planner] 开始{'重新' if is_re_planned else ''}执行规划器") logger.info(f"{self.log_prefix}开始想要做什么")
# --- 新增:检查历史动作并调整可用动作 ---
lian_xu_wen_ben_hui_fu = 0 # 连续文本回复次数
actions_to_remove_temporarily = [] actions_to_remove_temporarily = []
probability_roll = random.random() # 在循环外掷骰子一次,用于概率判断 # --- 检查历史动作并决定临时移除动作 (逻辑保持不变) ---
lian_xu_wen_ben_hui_fu = 0
# 反向遍历最近的循环历史 probability_roll = random.random()
for cycle in reversed(self._cycle_history): for cycle in reversed(self._cycle_history):
# 只关心实际执行了动作的循环
if cycle.action_taken: if cycle.action_taken:
if cycle.action_type == "text_reply": if cycle.action_type == "text_reply":
lian_xu_wen_ben_hui_fu += 1 lian_xu_wen_ben_hui_fu += 1
else: else:
break # 遇到非文本回复,中断计数 break
# 检查最近的3个循环即可避免检查过多历史 (如果历史很长)
if len(self._cycle_history) > 0 and cycle.cycle_id <= self._cycle_history[0].cycle_id + ( if len(self._cycle_history) > 0 and cycle.cycle_id <= self._cycle_history[0].cycle_id + (
len(self._cycle_history) - 4 len(self._cycle_history) - 4
): ):
break break
logger.debug(f"{self.log_prefix}[Planner] 检测到连续文本回复次数: {lian_xu_wen_ben_hui_fu}") logger.debug(f"{self.log_prefix}[Planner] 检测到连续文本回复次数: {lian_xu_wen_ben_hui_fu}")
# 根据连续次数决定临时移除哪些动作
if lian_xu_wen_ben_hui_fu >= 3: if lian_xu_wen_ben_hui_fu >= 3:
logger.info(f"{self.log_prefix}[Planner] 连续回复 >= 3 次,强制移除 text_reply 和 emoji_reply") logger.info(f"{self.log_prefix}[Planner] 连续回复 >= 3 次,强制移除 text_reply 和 emoji_reply")
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"]) actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
elif lian_xu_wen_ben_hui_fu == 2: elif lian_xu_wen_ben_hui_fu == 2:
if probability_roll < 0.8: # 80% 概率 if probability_roll < 0.8:
logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (触发)") logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (触发)")
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"]) actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
else: else:
@@ -826,183 +800,179 @@ class HeartFChatting:
f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (未触发)" f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (未触发)"
) )
elif lian_xu_wen_ben_hui_fu == 1: elif lian_xu_wen_ben_hui_fu == 1:
if probability_roll < 0.4: # 40% 概率 if probability_roll < 0.4:
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (触发)") logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (触发)")
actions_to_remove_temporarily.append("text_reply") actions_to_remove_temporarily.append("text_reply")
else: else:
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (未触发)") logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (未触发)")
# 如果 lian_xu_wen_ben_hui_fu == 0则不移除任何动作 # --- 结束检查历史动作 ---
# --- 结束:检查历史动作 ---
# 获取观察信息 # 获取观察信息
observation = self.observations[0] observation = self.observations[0]
if is_re_planned: # if is_re_planned: # 暂时简化,不处理重新规划
await observation.observe() # await observation.observe()
observed_messages = observation.talking_message observed_messages = observation.talking_message
observed_messages_str = observation.talking_message_str_truncate observed_messages_str = observation.talking_message_str_truncate
# --- 使用 LLM 进行决策 --- # # --- 使用 LLM 进行决策 (JSON 输出模式) --- #
reasoning = "默认决策或获取决策失败" action = "no_reply" # 默认动作
llm_error = False # LLM错误标志 reasoning = "规划器初始化默认"
arguments = None # 初始化参数变量 emoji_query = ""
emoji_query = "" # <--- 在这里初始化 emoji_query llm_error = False # LLM 请求或解析错误标志
# 获取我们将传递给 prompt 构建器和用于验证的当前可用动作
current_available_actions = self.action_manager.get_available_actions()
try: try:
# --- 新增:应用临时动作移除 --- # --- 应用临时动作移除 ---
if actions_to_remove_temporarily: if actions_to_remove_temporarily:
self.action_manager.temporarily_remove_actions(actions_to_remove_temporarily) self.action_manager.temporarily_remove_actions(actions_to_remove_temporarily)
# 更新 current_available_actions 以反映移除后的状态
current_available_actions = self.action_manager.get_available_actions()
logger.debug( logger.debug(
f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(self.action_manager.get_available_actions().keys())}" f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(current_available_actions.keys())}"
) )
# --- 构建提示词 --- # --- 构建提示词 (调用修改后的 _build_planner_prompt) ---
replan_prompt_str = "" # replan_prompt_str = "" # 暂时简化
if is_re_planned: # if is_re_planned:
replan_prompt_str = await self._build_replan_prompt( # replan_prompt_str = await self._build_replan_prompt(
self._current_cycle.action_type, self._current_cycle.reasoning # self._current_cycle.action_type, self._current_cycle.reasoning
) # )
prompt = await self._build_planner_prompt( prompt = await self._build_planner_prompt(
observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt_str observed_messages_str,
current_mind,
self.sub_mind.structured_info,
"", # replan_prompt_str,
current_available_actions, # <--- 传入当前可用动作
) )
# --- 调用 LLM --- # --- 调用 LLM (普通文本生成) ---
llm_content = None
try: try:
planner_tools = self.action_manager.get_planner_tool_definition() # 假设 LLMRequest 有 generate_response 方法返回 (content, reasoning, model_name)
logger.debug(f"{self.log_prefix}[Planner] 本次使用的工具定义: {planner_tools}") # 记录本次使用的工具 # 我们只需要 content
_response_text, _reasoning_content, tool_calls = await self.planner_llm.generate_response_tool_async( # !! 注意:这里假设 self.planner_llmgenerate_response 方法
prompt=prompt, # !! 如果你的 LLMRequest 类使用的是其他方法名,请相应修改
tools=planner_tools, llm_content, _, _ = await self.planner_llm.generate_response(prompt=prompt)
) logger.debug(f"{self.log_prefix}[Planner] LLM 原始 JSON 响应 (预期): {llm_content}")
logger.debug(f"{self.log_prefix}[Planner] 原始人 LLM响应: {_response_text}")
except Exception as req_e: except Exception as req_e:
logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}") logger.error(f"{self.log_prefix}[Planner] LLM 请求执行失败: {req_e}")
action = "error" reasoning = f"LLM 请求失败: {req_e}"
reasoning = f"LLM请求失败: {req_e}"
llm_error = True llm_error = True
# 直接返回错误结果 # 直接使用默认动作返回错误结果
return { action = "no_reply" # 明确设置为默认值
"action": action, emoji_query = "" # 明确设置为空
"reasoning": reasoning, # 不再立即返回,而是继续执行 finally 块以恢复动作
"emoji_query": "", # return { ... }
"current_mind": current_mind,
"observed_messages": observed_messages,
"llm_error": llm_error,
}
# 默认错误状态 # --- 解析 LLM 返回的 JSON (仅当 LLM 请求未出错时进行) ---
action = "error" if not llm_error and llm_content:
reasoning = "处理工具调用时出错" try:
llm_error = True # 尝试去除可能的 markdown 代码块标记
cleaned_content = (
llm_content.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
)
if not cleaned_content:
raise json.JSONDecodeError("Cleaned content is empty", cleaned_content, 0)
parsed_json = json.loads(cleaned_content)
# 1. 验证工具调用 # 提取决策,提供默认值
success, valid_tool_calls, error_msg = process_llm_tool_calls( extracted_action = parsed_json.get("action", "no_reply")
tool_calls, log_prefix=f"{self.log_prefix}[Planner] " extracted_reasoning = parsed_json.get("reasoning", "LLM未提供理由")
) extracted_emoji_query = parsed_json.get("emoji_query", "")
if success and valid_tool_calls: # 验证动作是否在当前可用列表中
# 2. 提取第一个调用并获取参数 # !! 使用调用 prompt 时实际可用的动作列表进行验证
first_tool_call = valid_tool_calls[0] if extracted_action not in current_available_actions:
tool_name = first_tool_call.get("function", {}).get("name")
arguments = extract_tool_call_arguments(first_tool_call, None)
# 3. 检查名称和参数
expected_tool_name = "decide_reply_action"
if tool_name == expected_tool_name and arguments is not None:
# 4. 成功,提取决策
extracted_action = arguments.get("action", "no_reply")
# 验证动作
if extracted_action not in self.action_manager.get_available_actions():
# 如果LLM返回了一个此时不该用的动作因为被临时移除了
# 或者完全无效的动作
logger.warning( logger.warning(
f"{self.log_prefix}[Planner] LLM返回了当前不可用或无效的动作: {extracted_action},将强制使用 'no_reply'" f"{self.log_prefix}[Planner] LLM 返回了当前不可用或无效的动作: '{extracted_action}' (可用: {list(current_available_actions.keys())}),将强制使用 'no_reply'"
) )
action = "no_reply" action = "no_reply"
reasoning = f"LLM返回了当前不可用的动作: {extracted_action}" reasoning = f"LLM 返回了当前不可用的动作 '{extracted_action}' (可用: {list(current_available_actions.keys())})。原始理由: {extracted_reasoning}"
emoji_query = "" emoji_query = ""
llm_error = False # 视为逻辑修正而非 LLM 错误 # 检查 no_reply 是否也恰好被移除了 (极端情况)
# --- 检查 'no_reply' 是否也恰好被移除了 (极端情况) --- if "no_reply" not in current_available_actions:
if "no_reply" not in self.action_manager.get_available_actions():
logger.error( logger.error(
f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。" f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。"
) )
action = "error" # 回退到错误状态 action = "error" # 回退到错误状态
reasoning = "无法执行任何有效动作,包括 no_reply" reasoning = "无法执行任何有效动作,包括 no_reply"
llm_error = True llm_error = True # 标记为严重错误
else:
llm_error = False # 视为逻辑修正而非 LLM 错误
else: else:
# 动作有效且可用,使用提取的值 # 动作有效且可用
action = extracted_action action = extracted_action
reasoning = arguments.get("reasoning", "未提供理由") reasoning = extracted_reasoning
emoji_query = arguments.get("emoji_query", "") emoji_query = extracted_emoji_query
llm_error = False # 成功处理 llm_error = False # 解析成功
# 记录决策结果
logger.debug( logger.debug(
f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'" f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果 (来自JSON): {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
) )
elif tool_name != expected_tool_name:
reasoning = f"LLM返回了非预期的工具: {tool_name}"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
else: # arguments is None
reasoning = f"无法提取工具 {tool_name} 的参数"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
elif not success:
reasoning = f"验证工具调用失败: {error_msg}"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
else: # not valid_tool_calls
# 如果没有有效的工具调用,我们需要检查 'no_reply' 是否是当前唯一可用的动作
available_actions = list(self.action_manager.get_available_actions().keys())
if available_actions == ["no_reply"]:
logger.info(
f"{self.log_prefix}[Planner] LLM未返回工具调用但当前唯一可用动作是 'no_reply',将执行 'no_reply'"
)
action = "no_reply"
reasoning = "LLM未返回工具调用且当前仅 'no_reply' 可用"
emoji_query = ""
llm_error = False # 视为逻辑选择而非错误
else:
reasoning = "LLM未返回有效的工具调用"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
# llm_error 保持为 True
# 如果 llm_error 仍然是 True说明在处理过程中有错误发生
except Exception as llm_e: except json.JSONDecodeError as json_e:
logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中发生意外错误: {llm_e}") logger.warning(
f"{self.log_prefix}[Planner] 解析LLM响应JSON失败: {json_e}. LLM原始输出: '{llm_content}'"
)
reasoning = f"解析LLM响应JSON失败: {json_e}. 将使用默认动作 'no_reply'."
action = "no_reply" # 解析失败则默认不回复
emoji_query = ""
llm_error = True # 标记解析错误
except Exception as parse_e:
logger.error(f"{self.log_prefix}[Planner] 处理LLM响应时发生意外错误: {parse_e}")
reasoning = f"处理LLM响应时发生意外错误: {parse_e}. 将使用默认动作 'no_reply'."
action = "no_reply"
emoji_query = ""
llm_error = True
elif not llm_error and not llm_content:
# LLM 请求成功但返回空内容
logger.warning(f"{self.log_prefix}[Planner] LLM 返回了空内容。")
reasoning = "LLM 返回了空内容,使用默认动作 'no_reply'."
action = "no_reply"
emoji_query = ""
llm_error = True # 标记为空响应错误
# 如果 llm_error 在此阶段为 True意味着请求成功但解析失败或返回空
# 如果 llm_error 在请求阶段就为 True则跳过了此解析块
except Exception as outer_e:
logger.error(f"{self.log_prefix}[Planner] Planner 处理过程中发生意外错误: {outer_e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
action = "error" action = "error" # 发生未知错误,标记为 error 动作
reasoning = f"Planner内部处理错误: {llm_e}" reasoning = f"Planner 内部处理错误: {outer_e}"
emoji_query = ""
llm_error = True llm_error = True
# --- 新增:确保动作恢复 ---
finally: finally:
if actions_to_remove_temporarily: # 只有当确实移除了动作时才需要恢复 # --- 确保动作恢复 ---
# 检查 self._original_actions_backup 是否有值来判断是否需要恢复
if self.action_manager._original_actions_backup is not None:
self.action_manager.restore_actions() self.action_manager.restore_actions()
logger.debug( logger.debug(
f"{self.log_prefix}[Planner] 恢复了原始动作集, 当前可用: {list(self.action_manager.get_available_actions().keys())}" f"{self.log_prefix}[Planner] 恢复了原始动作集, 当前可用: {list(self.action_manager.get_available_actions().keys())}"
) )
# --- 结束确保动作恢复 --- # --- 结束确保动作恢复 ---
# --- 新增:概率性忽略文本回复附带的表情(正确的位置)---
# --- 概率性忽略文本回复附带的表情 (逻辑保持不变) ---
if action == "text_reply" and emoji_query: if action == "text_reply" and emoji_query:
logger.debug(f"{self.log_prefix}[Planner] 大模型想让麦麦发文字时带表情: '{emoji_query}'") logger.debug(f"{self.log_prefix}[Planner] 大模型建议文字回复带表情: '{emoji_query}'")
# 掷骰子看看要不要听它的
if random.random() > EMOJI_SEND_PRO: if random.random() > EMOJI_SEND_PRO:
logger.info( logger.info(
f"{self.log_prefix}[Planner] 但是麦麦这次不想加表情 ({1 - EMOJI_SEND_PRO:.0%}),忽略表情 '{emoji_query}'" f"{self.log_prefix}但是麦麦这次不想加表情 ({1 - EMOJI_SEND_PRO:.0%}),忽略表情 '{emoji_query}'"
) )
emoji_query = "" # 表情请求清空,就不发了 emoji_query = "" # 清空表情请求
else: else:
logger.info(f"{self.log_prefix}[Planner] 好吧,加上表情 '{emoji_query}'") logger.info(f"{self.log_prefix}好吧,加上表情 '{emoji_query}'")
# --- 结束概率性忽略 --- # --- 结束概率性忽略 ---
# --- 结束 LLM 决策 --- #
# 返回结果字典
return { return {
"action": action, "action": action,
"reasoning": reasoning, "reasoning": reasoning,
"emoji_query": emoji_query, "emoji_query": emoji_query,
"current_mind": current_mind, "current_mind": current_mind,
"observed_messages": observed_messages, "observed_messages": observed_messages,
"llm_error": llm_error, "llm_error": llm_error, # 返回错误状态
} }
async def _get_anchor_message(self) -> Optional[MessageRecv]: async def _get_anchor_message(self) -> Optional[MessageRecv]:
@@ -1031,9 +1001,7 @@ class HeartFChatting:
} }
anchor_message = MessageRecv(placeholder_msg_dict) anchor_message = MessageRecv(placeholder_msg_dict)
anchor_message.update_chat_stream(self.chat_stream) anchor_message.update_chat_stream(self.chat_stream)
logger.info( logger.debug(f"{self.log_prefix} 创建占位符锚点消息: ID={anchor_message.message_info.message_id}")
f"{self.log_prefix} Created placeholder anchor message: ID={anchor_message.message_info.message_id}"
)
return anchor_message return anchor_message
except Exception as e: except Exception as e:
@@ -1146,8 +1114,9 @@ class HeartFChatting:
current_mind: Optional[str], current_mind: Optional[str],
structured_info: Dict[str, Any], structured_info: Dict[str, Any],
replan_prompt: str, replan_prompt: str,
current_available_actions: Dict[str, str],
) -> str: ) -> str:
"""构建 Planner LLM 的提示词""" """构建 Planner LLM 的提示词 (获取模板并填充数据)"""
try: try:
# 准备结构化信息块 # 准备结构化信息块
structured_info_block = "" structured_info_block = ""
@@ -1163,12 +1132,13 @@ class HeartFChatting:
else: else:
chat_content_block = "当前没有观察到新的聊天内容。\n" chat_content_block = "当前没有观察到新的聊天内容。\n"
# 准备当前思维块 # 准备当前思维块 (修改以匹配模板)
current_mind_block = "" current_mind_block = ""
if current_mind: if current_mind:
current_mind_block = f"{current_mind}" # 模板中占位符是 {current_mind_block},它期望包含"你的内心想法:"的前缀
current_mind_block = f"你的内心想法:\n{current_mind}"
else: else:
current_mind_block = "[没有特别的想法]" current_mind_block = "你的内心想法:\n[没有特别的想法]"
# 准备循环信息块 (分析最近的活动循环) # 准备循环信息块 (分析最近的活动循环)
recent_active_cycles = [] recent_active_cycles = []
@@ -1208,23 +1178,40 @@ class HeartFChatting:
# 包装提示块,增加可读性,即使没有连续回复也给个标记 # 包装提示块,增加可读性,即使没有连续回复也给个标记
if cycle_info_block: if cycle_info_block:
# 模板中占位符是 {cycle_info_block},它期望包含"【近期回复历史】"的前缀
cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n" cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n"
else: else:
# 如果最近的活动循环不是文本回复,或者没有活动循环 # 如果最近的活动循环不是文本回复,或者没有活动循环
cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n" cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n"
individuality = Individuality.get_instance() individuality = Individuality.get_instance()
# 模板中占位符是 {prompt_personality}
prompt_personality = individuality.get_prompt(x_person=2, level=2) prompt_personality = individuality.get_prompt(x_person=2, level=2)
# 获取提示词模板并填充数据 # --- 构建可用动作描述 (用于填充模板中的 {action_options_text}) ---
prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format( action_options_text = "当前你可以选择的行动有:\n"
action_keys = list(current_available_actions.keys())
for name in action_keys:
desc = current_available_actions[name]
action_options_text += f"- '{name}': {desc}\n"
# --- 选择一个示例动作键 (用于填充模板中的 {example_action}) ---
example_action_key = action_keys[0] if action_keys else "no_reply"
# --- 获取提示词模板 ---
planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt")
# --- 填充模板 ---
prompt = planner_prompt_template.format(
bot_name=global_config.BOT_NICKNAME, bot_name=global_config.BOT_NICKNAME,
prompt_personality=prompt_personality, prompt_personality=prompt_personality,
structured_info_block=structured_info_block, structured_info_block=structured_info_block,
chat_content_block=chat_content_block, chat_content_block=chat_content_block,
current_mind_block=current_mind_block, current_mind_block=current_mind_block,
replan=replan_prompt, replan="", # 暂时留空 replan 信息
cycle_info_block=cycle_info_block, cycle_info_block=cycle_info_block,
action_options_text=action_options_text, # 传入可用动作描述
example_action=example_action_key, # 传入示例动作键
) )
return prompt return prompt
@@ -1232,7 +1219,7 @@ class HeartFChatting:
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}") logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return "" return "[构建 Planner Prompt 时出错]" # 返回错误提示,避免空字符串
# --- 回复器 (Replier) 的定义 --- # # --- 回复器 (Replier) 的定义 --- #
async def _replier_work( async def _replier_work(
@@ -1273,7 +1260,7 @@ class HeartFChatting:
try: try:
with Timer("LLM生成", {}): # 内部计时器,可选保留 with Timer("LLM生成", {}): # 内部计时器,可选保留
content, reasoning_content, model_name = await self.model_normal.generate_response(prompt) content, reasoning_content, model_name = await self.model_normal.generate_response(prompt)
logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n") # logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n")
# 捕捉 LLM 输出信息 # 捕捉 LLM 输出信息
info_catcher.catch_after_llm_generated( info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name

View File

@@ -47,17 +47,15 @@ def init_prompt():
"info_from_tools", "info_from_tools",
) )
# Planner提示词 - 优化版 # Planner提示词 - 修改为要求 JSON 输出
Prompt( Prompt(
"""你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话: """你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话:
{structured_info_block} {structured_info_block}
{chat_content_block} {chat_content_block}
你的内心想法:
{current_mind_block} {current_mind_block}
{replan}
{cycle_info_block} {cycle_info_block}
请综合分析聊天内容和你看到的新消息,参考内心想法,使用'decide_reply_action'工具做出决策。决策时请注意: 请综合分析聊天内容和你看到的新消息,参考内心想法,并根据以下原则和可用动作做出决策。
【回复原则】 【回复原则】
1. 不回复(no_reply)适用: 1. 不回复(no_reply)适用:
@@ -81,14 +79,34 @@ def init_prompt():
- 避免重复或评价自己的发言 - 避免重复或评价自己的发言
- 不要和自己聊天 - 不要和自己聊天
必须遵守 决策任务
- 遵守回复原则 {action_options_text}
- 必须调用工具并包含action和reasoning
- 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply) 你必须从上面列出的可用行动中选择一个,并说明原因。
- 并不是所有选择都可用 你的决策必须以严格的 JSON 格式输出,且仅包含 JSON 内容,不要有任何其他文字或解释。
- 选择text_reply或emoji_reply时必须提供emoji_query JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
- 保持回复自然,符合日常聊天习惯""", {{
"planner_prompt", "action": "string", // 必须是上面提供的可用行动之一 (例如: '{example_action}')
"reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则
"emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。
}}
例如:
{{
"action": "text_reply",
"reasoning": "用户提到了我,且问题比较具体,适合用文本回复。考虑到内容,可以带上一个微笑表情。",
"emoji_query": "微笑"
}}
{{
"action": "no_reply",
"reasoning": "我已经连续回复了两次,而且这个话题我不太感兴趣,根据回复原则,选择不回复,等待其他人发言。",
"emoji_query": ""
}}
请输出你的决策 JSON
""", # 使用三引号避免内部引号问题
"planner_prompt", # 保持名称不变,替换内容
) )
Prompt( Prompt(
@@ -157,10 +175,13 @@ class PromptBuilder:
current_mind_info, current_mind_info,
structured_info, structured_info,
chat_stream, chat_stream,
sender_name,
) )
return None return None
async def _build_prompt_focus(self, reason, current_mind_info, structured_info, chat_stream) -> tuple[str, str]: async def _build_prompt_focus(
self, reason, current_mind_info, structured_info, chat_stream, sender_name
) -> tuple[str, str]:
individuality = Individuality.get_instance() individuality = Individuality.get_instance()
prompt_personality = individuality.get_prompt(x_person=0, level=2) prompt_personality = individuality.get_prompt(x_person=0, level=2)
# 日程构建 # 日程构建
@@ -240,6 +261,7 @@ class PromptBuilder:
reason=reason, reason=reason,
prompt_ger=prompt_ger, prompt_ger=prompt_ger,
moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
sender_name=sender_name,
) )
logger.debug(f"focus_chat_prompt: \n{prompt}") logger.debug(f"focus_chat_prompt: \n{prompt}")

View File

@@ -358,7 +358,9 @@ class NormalChat:
processed_count = 0 processed_count = 0
# --- 修改迭代前创建要处理的ID列表副本防止迭代时修改 --- # --- 修改迭代前创建要处理的ID列表副本防止迭代时修改 ---
messages_to_process_initially = list(messages_to_reply) # 创建副本 messages_to_process_initially = list(messages_to_reply) # 创建副本
# --- 修改结束 --- # --- 新增:限制最多处理两条消息 ---
messages_to_process_initially = messages_to_process_initially[:2]
# --- 新增结束 ---
for item in messages_to_process_initially: # 使用副本迭代 for item in messages_to_process_initially: # 使用副本迭代
msg_id, (message, interest_value, is_mentioned) = item msg_id, (message, interest_value, is_mentioned) = item
# --- 修改:在处理前尝试 pop防止竞争 --- # --- 修改:在处理前尝试 pop防止竞争 ---
@@ -443,7 +445,7 @@ class NormalChat:
logger.error(f"[{self.stream_name}] 任务异常: {exc}") logger.error(f"[{self.stream_name}] 任务异常: {exc}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info(f"[{self.stream_name}] 任务已取消") logger.debug(f"[{self.stream_name}] 任务已取消")
except Exception as e: except Exception as e:
logger.error(f"[{self.stream_name}] 回调处理错误: {e}") logger.error(f"[{self.stream_name}] 回调处理错误: {e}")
finally: finally:
@@ -456,12 +458,12 @@ class NormalChat:
"""停止当前实例的兴趣监控任务。""" """停止当前实例的兴趣监控任务。"""
if self._chat_task and not self._chat_task.done(): if self._chat_task and not self._chat_task.done():
task = self._chat_task task = self._chat_task
logger.info(f"[{self.stream_name}] 尝试取消聊天任务。") logger.debug(f"[{self.stream_name}] 尝试取消normal聊天任务。")
task.cancel() task.cancel()
try: try:
await task # 等待任务响应取消 await task # 等待任务响应取消
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info(f"[{self.stream_name}] 聊天任务已成功取消") logger.info(f"[{self.stream_name}] 结束一般聊天模式")
except Exception as e: except Exception as e:
# 回调函数 _handle_task_completion 会处理异常日志 # 回调函数 _handle_task_completion 会处理异常日志
logger.warning(f"[{self.stream_name}] 等待监控任务取消时捕获到异常 (可能已在回调中记录): {e}") logger.warning(f"[{self.stream_name}] 等待监控任务取消时捕获到异常 (可能已在回调中记录): {e}")

View File

@@ -82,12 +82,14 @@ class NormalChatGenerator:
sender_name=sender_name, sender_name=sender_name,
chat_stream=message.chat_stream, chat_stream=message.chat_stream,
) )
logger.info(f"构建prompt时间: {t_build_prompt.human_readable}") logger.debug(f"构建prompt时间: {t_build_prompt.human_readable}")
try: try:
content, reasoning_content, self.current_model_name = await model.generate_response(prompt) content, reasoning_content, self.current_model_name = await model.generate_response(prompt)
logger.info(f"prompt:{prompt}\n生成回复:{content}") logger.debug(f"prompt:{prompt}\n生成回复:{content}")
logger.info(f"{message.processed_plain_text} 的回复:{content}")
info_catcher.catch_after_llm_generated( info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name

View File

@@ -104,8 +104,8 @@ mentioned_bot_inevitable_reply = false # 提及 bot 必然回复
at_bot_inevitable_reply = false # @bot 必然回复 at_bot_inevitable_reply = false # @bot 必然回复
[focus_chat] #专注聊天 [focus_chat] #专注聊天
reply_trigger_threshold = 3.5 # 专注聊天触发阈值,越低越容易进入专注聊天 reply_trigger_threshold = 3.6 # 专注聊天触发阈值,越低越容易进入专注聊天
default_decay_rate_per_second = 0.98 # 默认衰减率,越大衰减越快,越高越难进入专注聊天 default_decay_rate_per_second = 0.95 # 默认衰减率,越大衰减越快,越高越难进入专注聊天
consecutive_no_reply_threshold = 3 # 连续不回复的阈值,越低越容易结束专注聊天 consecutive_no_reply_threshold = 3 # 连续不回复的阈值,越低越容易结束专注聊天
# 以下选项暂时无效 # 以下选项暂时无效