From 635ead2b6a4af4d6b55dccaca34ba4cdacae833e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 1 May 2025 15:09:56 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 9 ++- src/config/config.py | 18 +++-- src/plugins/group_nickname/nickname_mapper.py | 30 ++++---- .../group_nickname/nickname_processor.py | 75 ++++++++++--------- src/plugins/group_nickname/nickname_utils.py | 32 ++++---- src/plugins/heartFC_chat/heartFC_chat.py | 57 ++++++++------ .../heartFC_chat/heartflow_prompt_builder.py | 10 +-- .../person_info/relationship_manager.py | 56 ++++++++------ 8 files changed, 161 insertions(+), 126 deletions(-) diff --git a/bot.py b/bot.py index dcd2a3e1..0a3c46e8 100644 --- a/bot.py +++ b/bot.py @@ -13,7 +13,10 @@ from src.common.logger_manager import get_logger # from src.common.logger import LogConfig, CONFIRM_STYLE_CONFIG from src.common.crash_logger import install_crash_handler from src.main import MainSystem -from src.plugins.group_nickname.nickname_processor import start_nickname_processor, stop_nickname_processor # <--- 添加这行导入 +from src.plugins.group_nickname.nickname_processor import ( + start_nickname_processor, + stop_nickname_processor, +) # <--- 添加这行导入 import atexit logger = get_logger("main") @@ -230,11 +233,11 @@ if __name__ == "__main__": # 在这里启动绰号处理进程 logger.info("准备启动绰号处理线程...") - start_nickname_processor() # <--- 添加启动调用 + start_nickname_processor() # <--- 添加启动调用 logger.info("已调用启动绰号处理线程。") # 注册退出处理函数 (确保进程能被关闭) - atexit.register(stop_nickname_processor) # <--- 在这里注册停止函数 + atexit.register(stop_nickname_processor) # <--- 在这里注册停止函数 logger.info("已注册绰号处理线程的退出处理程序。") # 创建事件循环 diff --git a/src/config/config.py b/src/config/config.py index c3cae240..b440f01d 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -271,11 +271,11 @@ class BotConfig: enable_pfc_chatting: bool = False # 是否启用PFC聊天 # Group Nickname - ENABLE_NICKNAME_MAPPING: bool = False # 绰号映射功能总开关 - MAX_NICKNAMES_IN_PROMPT: int = 10 # Prompt 中最多注入的绰号数量 - NICKNAME_PROBABILITY_SMOOTHING: int = 1 # 绰号加权随机选择的平滑因子 - NICKNAME_QUEUE_MAX_SIZE: int = 100 # 绰号处理队列最大容量 - NICKNAME_PROCESS_SLEEP_INTERVAL: float = 0.5 # 绰号处理进程休眠间隔(秒) + ENABLE_NICKNAME_MAPPING: bool = False # 绰号映射功能总开关 + MAX_NICKNAMES_IN_PROMPT: int = 10 # Prompt 中最多注入的绰号数量 + NICKNAME_PROBABILITY_SMOOTHING: int = 1 # 绰号加权随机选择的平滑因子 + NICKNAME_QUEUE_MAX_SIZE: int = 100 # 绰号处理队列最大容量 + NICKNAME_PROCESS_SLEEP_INTERVAL: float = 0.5 # 绰号处理进程休眠间隔(秒) # 模型配置 llm_reasoning: dict[str, str] = field(default_factory=lambda: {}) @@ -410,9 +410,13 @@ class BotConfig: gn_config = parent.get("group_nickname", {}) config.ENABLE_NICKNAME_MAPPING = gn_config.get("enable_nickname_mapping", config.ENABLE_NICKNAME_MAPPING) config.MAX_NICKNAMES_IN_PROMPT = gn_config.get("max_nicknames_in_prompt", config.MAX_NICKNAMES_IN_PROMPT) - config.NICKNAME_PROBABILITY_SMOOTHING = gn_config.get("nickname_probability_smoothing", config.NICKNAME_PROBABILITY_SMOOTHING) + config.NICKNAME_PROBABILITY_SMOOTHING = gn_config.get( + "nickname_probability_smoothing", config.NICKNAME_PROBABILITY_SMOOTHING + ) config.NICKNAME_QUEUE_MAX_SIZE = gn_config.get("nickname_queue_max_size", config.NICKNAME_QUEUE_MAX_SIZE) - config.NICKNAME_PROCESS_SLEEP_INTERVAL = gn_config.get("nickname_process_sleep_interval", config.NICKNAME_PROCESS_SLEEP_INTERVAL) + config.NICKNAME_PROCESS_SLEEP_INTERVAL = gn_config.get( + "nickname_process_sleep_interval", config.NICKNAME_PROCESS_SLEEP_INTERVAL + ) def bot(parent: dict): # 机器人基础配置 diff --git a/src/plugins/group_nickname/nickname_mapper.py b/src/plugins/group_nickname/nickname_mapper.py index b4cfe072..283f8090 100644 --- a/src/plugins/group_nickname/nickname_mapper.py +++ b/src/plugins/group_nickname/nickname_mapper.py @@ -2,6 +2,7 @@ import json from typing import Dict, Any, Optional from src.common.logger_manager import get_logger from src.plugins.models.utils_model import LLMRequest + # 从全局配置导入 from src.config.config import global_config @@ -9,7 +10,7 @@ from src.config.config import global_config logger = get_logger("nickname_mapper") llm_mapper: Optional[LLMRequest] = None -if global_config.ENABLE_NICKNAME_MAPPING: # 使用全局开关 +if global_config.ENABLE_NICKNAME_MAPPING: # 使用全局开关 try: # 从全局配置获取模型设置 model_config = global_config.llm_nickname_mapping @@ -17,10 +18,10 @@ if global_config.ENABLE_NICKNAME_MAPPING: # 使用全局开关 logger.error("在全局配置中未找到有效的 'llm_nickname_mapping' 配置或缺少 'name' 字段。") else: llm_mapper = LLMRequest( # <-- LLM 初始化 - model=global_config.llm_nickname_mapping, - temperature=global_config.llm_nickname_mapping["temp"], - max_tokens=256, - request_type="nickname_mapping", + model=global_config.llm_nickname_mapping, + temperature=global_config.llm_nickname_mapping["temp"], + max_tokens=256, + request_type="nickname_mapping", ) logger.info("绰号映射 LLM 初始化成功 (使用全局配置)。") @@ -28,6 +29,7 @@ if global_config.ENABLE_NICKNAME_MAPPING: # 使用全局开关 logger.error(f"使用全局配置初始化绰号映射 LLM 失败: {e}", exc_info=True) llm_mapper = None + def _build_mapping_prompt(chat_history_str: str, bot_reply: str, user_name_map: Dict[str, str]) -> str: """构建用于 LLM 绰号映射的 Prompt""" # user_name_map 包含了 user_id 到 person_name (或 fallback nickname) 的映射 @@ -74,7 +76,7 @@ def _build_mapping_prompt(chat_history_str: str, bot_reply: str, user_name_map: async def analyze_chat_for_nicknames( chat_history_str: str, bot_reply: str, - user_name_map: Dict[str, str] # 这个 map 包含了 user_id -> person_name 的信息 + user_name_map: Dict[str, str], # 这个 map 包含了 user_id -> person_name 的信息 ) -> Dict[str, Any]: """ 调用 LLM 分析聊天记录和 Bot 回复,提取可靠的 用户ID-绰号 映射,并进行过滤。 @@ -111,13 +113,13 @@ async def analyze_chat_for_nicknames( result = json.loads(response_content) if isinstance(result, dict) and "is_exist" in result: if result["is_exist"] is True: - original_data = result.get("data") # 使用 .get() 更安全 - if isinstance(original_data, dict) and original_data: # 确保 data 是非空字典 + original_data = result.get("data") # 使用 .get() 更安全 + if isinstance(original_data, dict) and original_data: # 确保 data 是非空字典 logger.info(f"LLM 找到的原始绰号映射: {original_data}") # --- 开始过滤 --- filtered_data = {} - bot_qq_str = str(global_config.BOT_QQ) # 将机器人QQ转为字符串以便比较 + bot_qq_str = str(global_config.BOT_QQ) # 将机器人QQ转为字符串以便比较 for user_id, nickname in original_data.items(): # 检查 user_id 是否是字符串,以防万一 @@ -131,9 +133,11 @@ async def analyze_chat_for_nicknames( continue # 条件 2: 排除 nickname 与 person_name 相同的情况 - person_name = user_name_map.get(user_id) # 从传入的映射中查找 person_name + person_name = user_name_map.get(user_id) # 从传入的映射中查找 person_name if person_name and person_name == nickname: - logger.debug(f"过滤掉用户 {user_id} 的映射: 绰号 '{nickname}' 与其名称 '{person_name}' 相同。") + logger.debug( + f"过滤掉用户 {user_id} 的映射: 绰号 '{nickname}' 与其名称 '{person_name}' 相同。" + ) continue # 如果通过所有过滤条件,则保留 @@ -146,7 +150,7 @@ async def analyze_chat_for_nicknames( return {"is_exist": False} else: logger.info(f"过滤后的绰号映射: {filtered_data}") - return {"is_exist": True, "data": filtered_data} # 返回过滤后的数据 + return {"is_exist": True, "data": filtered_data} # 返回过滤后的数据 else: # is_exist 为 True 但 data 缺失、不是字典或为空 @@ -154,7 +158,7 @@ async def analyze_chat_for_nicknames( logger.warning("LLM 响应格式错误: is_exist 为 True 但 'data' 键缺失。") elif not isinstance(result.get("data"), dict): logger.warning("LLM 响应格式错误: is_exist 为 True 但 'data' 不是字典。") - else: # data 为空字典 + else: # data 为空字典 logger.debug("LLM 指示 is_exist=True 但 data 为空字典。视为 False 处理。") return {"is_exist": False} elif result["is_exist"] is False: diff --git a/src/plugins/group_nickname/nickname_processor.py b/src/plugins/group_nickname/nickname_processor.py index df68fb78..264654af 100644 --- a/src/plugins/group_nickname/nickname_processor.py +++ b/src/plugins/group_nickname/nickname_processor.py @@ -6,9 +6,9 @@ import threading import queue from typing import Dict, Optional -from pymongo.errors import OperationFailure, DuplicateKeyError # 引入 DuplicateKeyError +from pymongo.errors import OperationFailure, DuplicateKeyError # 引入 DuplicateKeyError from src.common.logger_manager import get_logger -from src.common.database import db # 使用全局 db +from src.common.database import db # 使用全局 db from .nickname_mapper import analyze_chat_for_nicknames from src.config.config import global_config @@ -16,6 +16,7 @@ logger = get_logger("nickname_processor") _stop_event = threading.Event() + async def update_nickname_counts(platform: str, group_id: str, nickname_map: Dict[str, str]): """ 更新数据库中用户的群组绰号计数 (使用全局 db)。 @@ -32,7 +33,7 @@ async def update_nickname_counts(platform: str, group_id: str, nickname_map: Dic from ..person_info.person_info import person_info_manager except ImportError: logger.error("无法导入 person_info_manager,无法生成 person_id!") - return # 无法继续,因为需要 person_id + return # 无法继续,因为需要 person_id person_info_collection = db.person_info @@ -67,16 +68,16 @@ async def update_nickname_counts(platform: str, group_id: str, nickname_map: Dic # 如果文档不存在,它会被创建,并设置 $setOnInsert 中的字段。 # 如果文档已存在,此操作不会修改任何内容(因为没有 $set 操作符)。 upsert_result = person_info_collection.update_one( - {"person_id": person_id}, # Filter by the unique key + {"person_id": person_id}, # Filter by the unique key { "$setOnInsert": { "person_id": person_id, "user_id": user_id_int, "platform": platform, - "group_nicknames": [] # 初始化 group_nicknames 数组 + "group_nicknames": [], # 初始化 group_nicknames 数组 } }, - upsert=True + upsert=True, ) # 可选日志:记录是否创建了新文档 @@ -91,28 +92,28 @@ async def update_nickname_counts(platform: str, group_id: str, nickname_map: Dic # 3a. 尝试增加现有群组中现有绰号的计数 update_result_inc = person_info_collection.update_one( { - "person_id": person_id, # 明确目标文档 - "group_nicknames": { # 检查数组中是否有匹配项 + "person_id": person_id, # 明确目标文档 + "group_nicknames": { # 检查数组中是否有匹配项 "$elemMatch": {"group_id": group_id_str, "nicknames.name": nickname} - } + }, }, - {"$inc": {"group_nicknames.$[group].nicknames.$[nick].count": 1}}, # 增加计数 - array_filters=[ # 指定要更新的数组元素 + {"$inc": {"group_nicknames.$[group].nicknames.$[nick].count": 1}}, # 增加计数 + array_filters=[ # 指定要更新的数组元素 {"group.group_id": group_id_str}, - {"nick.name": nickname} - ] + {"nick.name": nickname}, + ], ) # 3b. 如果上一步未修改 (绰号不存在于该群组),尝试将新绰号添加到现有群组 if update_result_inc.modified_count == 0: update_result_push_nick = person_info_collection.update_one( { - "person_id": person_id, # 明确目标文档 - "group_nicknames.group_id": group_id_str # 检查群组是否存在 + "person_id": person_id, # 明确目标文档 + "group_nicknames.group_id": group_id_str, # 检查群组是否存在 }, # 将新绰号添加到匹配群组的 nicknames 数组中 {"$push": {"group_nicknames.$[group].nicknames": {"name": nickname, "count": 1}}}, - array_filters=[{"group.group_id": group_id_str}] # 指定要推送到的群组 + array_filters=[{"group.group_id": group_id_str}], # 指定要推送到的群组 ) # 3c. 如果上一步也未修改 (群组条目本身不存在),则添加新的群组条目和绰号 @@ -120,22 +121,22 @@ async def update_nickname_counts(platform: str, group_id: str, nickname_map: Dic # 确保 group_nicknames 数组存在 (如果 $setOnInsert 失败或数据不一致时的保险措施) person_info_collection.update_one( {"person_id": person_id, "group_nicknames": {"$exists": False}}, - {"$set": {"group_nicknames": []}} + {"$set": {"group_nicknames": []}}, ) # 推送新的群组对象到 group_nicknames 数组 update_result_push_group = person_info_collection.update_one( { - "person_id": person_id, # 明确目标文档 - "group_nicknames.group_id": {"$ne": group_id_str} # 确保该群组 ID 尚未存在 + "person_id": person_id, # 明确目标文档 + "group_nicknames.group_id": {"$ne": group_id_str}, # 确保该群组 ID 尚未存在 }, { - "$push": { # 添加新的群组条目 + "$push": { # 添加新的群组条目 "group_nicknames": { "group_id": group_id_str, - "nicknames": [{"name": nickname, "count": 1}] # 初始化绰号列表 + "nicknames": [{"name": nickname, "count": 1}], # 初始化绰号列表 } } - } + }, ) if update_result_push_group.modified_count > 0: logger.debug(f"为 person_id {person_id} 添加了新的群组 {group_id_str} 和绰号 '{nickname}'") @@ -143,26 +144,27 @@ async def update_nickname_counts(platform: str, group_id: str, nickname_map: Dic except DuplicateKeyError as dk_err: # 这个错误理论上不应该再由步骤 2 的 upsert 触发。 # 如果仍然出现,可能指示 person_id 生成逻辑问题或非常罕见的 MongoDB 内部情况。 - logger.error(f"数据库操作失败 (DuplicateKeyError): person_id {person_id}. 错误: {dk_err}. 这不应该发生,请检查 person_id 生成逻辑和数据库状态。") + logger.error( + f"数据库操作失败 (DuplicateKeyError): person_id {person_id}. 错误: {dk_err}. 这不应该发生,请检查 person_id 生成逻辑和数据库状态。" + ) except OperationFailure as op_err: - logger.exception(f"数据库操作失败 (OperationFailure): 用户 {user_id_str}, 群组 {group_id_str}, 绰号 {nickname}({op_err})") + logger.exception( + f"数据库操作失败 (OperationFailure): 用户 {user_id_str}, 群组 {group_id_str}, 绰号 {nickname}({op_err})" + ) except Exception as e: logger.exception(f"更新用户 {user_id_str} 的绰号 '{nickname}' 时发生意外错误:{e}") # --- 使用 queue.Queue --- -queue_max_size = getattr(global_config, 'NICKNAME_QUEUE_MAX_SIZE', 100) +queue_max_size = getattr(global_config, "NICKNAME_QUEUE_MAX_SIZE", 100) nickname_queue: queue.Queue = queue.Queue(maxsize=queue_max_size) _nickname_thread: Optional[threading.Thread] = None + # --- add_to_nickname_queue (保持不变,已包含 platform) --- async def add_to_nickname_queue( - chat_history_str: str, - bot_reply: str, - platform: str, - group_id: Optional[str], - user_name_map: Dict[str, str] + chat_history_str: str, bot_reply: str, platform: str, group_id: Optional[str], user_name_map: Dict[str, str] ): """将需要分析的数据放入队列。""" if not global_config or not global_config.ENABLE_NICKNAME_MAPPING: @@ -173,7 +175,9 @@ async def add_to_nickname_queue( try: item = (chat_history_str, bot_reply, platform, str(group_id), user_name_map) nickname_queue.put_nowait(item) - logger.debug(f"已将项目添加到平台 '{platform}' 群组 '{group_id}' 的绰号队列。当前大小: {nickname_queue.qsize()}") + logger.debug( + f"已将项目添加到平台 '{platform}' 群组 '{group_id}' 的绰号队列。当前大小: {nickname_queue.qsize()}" + ) except queue.Full: logger.warning(f"无法将项目添加到绰号队列:队列已满 (maxsize={nickname_queue.maxsize})。") except Exception as e: @@ -185,7 +189,7 @@ async def _nickname_processing_loop(q: queue.Queue, stop_event: threading.Event) """独立线程中的主循环,处理队列任务 (使用全局 db 和 config)。""" thread_id = threading.get_ident() logger.info(f"绰号处理循环已启动 (线程 ID: {thread_id})。") - sleep_interval = getattr(global_config, 'NICKNAME_PROCESS_SLEEP_INTERVAL', 0.5) + sleep_interval = getattr(global_config, "NICKNAME_PROCESS_SLEEP_INTERVAL", 0.5) while not stop_event.is_set(): try: @@ -262,15 +266,14 @@ def start_nickname_processor(): stop_event = get_stop_event() stop_event.clear() _nickname_thread = threading.Thread( - target=_run_processor_thread, - args=(nickname_queue, stop_event), - daemon=True + target=_run_processor_thread, args=(nickname_queue, stop_event), daemon=True ) _nickname_thread.start() logger.info(f"绰号处理器线程已启动 (Thread ID: {_nickname_thread.ident})") else: logger.warning("绰号处理器线程已在运行中。") + # --- stop_nickname_processor (保持不变) --- def stop_nickname_processor(): """停止绰号映射处理线程。""" @@ -293,11 +296,13 @@ def stop_nickname_processor(): else: logger.info("绰号处理器线程未在运行或已被清理。") + # --- Event 控制函数 (保持不变) --- def get_stop_event() -> threading.Event: """获取全局停止事件""" return _stop_event + def set_stop_event(): """设置全局停止事件,通知子线程退出""" _stop_event.set() diff --git a/src/plugins/group_nickname/nickname_utils.py b/src/plugins/group_nickname/nickname_utils.py index ced2f6ca..230ce8c4 100644 --- a/src/plugins/group_nickname/nickname_utils.py +++ b/src/plugins/group_nickname/nickname_utils.py @@ -6,9 +6,8 @@ from src.config.config import global_config logger = get_logger("nickname_utils") -def select_nicknames_for_prompt( - all_nicknames_info: Dict[str, List[Dict[str, int]]] -) -> List[Tuple[str, str, int]]: + +def select_nicknames_for_prompt(all_nicknames_info: Dict[str, List[Dict[str, int]]]) -> List[Tuple[str, str, int]]: """ 从给定的绰号信息中,根据映射次数加权随机选择最多 N 个绰号。 @@ -36,11 +35,12 @@ def select_nicknames_for_prompt( weight = count + global_config.NICKNAME_PROBABILITY_SMOOTHING candidates.append((user_name, nickname, count, weight)) else: - logger.warning(f"Invalid count for nickname '{nickname}' of user '{user_name}': {count}. Skipping.") + logger.warning( + f"Invalid count for nickname '{nickname}' of user '{user_name}': {count}. Skipping." + ) else: logger.warning(f"Invalid nickname entry format for user '{user_name}': {nickname_entry}. Skipping.") - if not candidates: return [] @@ -49,8 +49,8 @@ def select_nicknames_for_prompt( if total_weight <= 0: # 如果所有权重都无效或为0,则随机选择(或按次数选择) - candidates.sort(key=lambda x: x[2], reverse=True) # 按原始次数排序 - selected = candidates[:global_config.MAX_NICKNAMES_IN_PROMPT] + candidates.sort(key=lambda x: x[2], reverse=True) # 按原始次数排序 + selected = candidates[: global_config.MAX_NICKNAMES_IN_PROMPT] else: # 计算归一化概率 probabilities = [c[3] / total_weight for c in candidates] @@ -64,7 +64,7 @@ def select_nicknames_for_prompt( selected_indices = set() selected = [] attempts = 0 - max_attempts = num_to_select * 5 # 防止无限循环 + max_attempts = num_to_select * 5 # 防止无限循环 while len(selected) < num_to_select and attempts < max_attempts: # 每次只选一个,避免一次选多个时概率分布变化导致的问题 @@ -77,20 +77,21 @@ def select_nicknames_for_prompt( # 如果尝试多次后仍未选够,补充出现次数最多的 if len(selected) < num_to_select: remaining_candidates = [c for i, c in enumerate(candidates) if i not in selected_indices] - remaining_candidates.sort(key=lambda x: x[2], reverse=True) # 按原始次数排序 + remaining_candidates.sort(key=lambda x: x[2], reverse=True) # 按原始次数排序 needed = num_to_select - len(selected) selected.extend(remaining_candidates[:needed]) except Exception as e: - logger.error(f"Error during weighted random choice for nicknames: {e}. Falling back to top N.", exc_info=True) + logger.error( + f"Error during weighted random choice for nicknames: {e}. Falling back to top N.", exc_info=True + ) # 出错时回退到选择次数最多的 N 个 candidates.sort(key=lambda x: x[2], reverse=True) - selected = candidates[:global_config.MAX_NICKNAMES_IN_PROMPT] - + selected = candidates[: global_config.MAX_NICKNAMES_IN_PROMPT] # 格式化输出并按次数排序 result = [(user, nick, count) for user, nick, count, _weight in selected] - result.sort(key=lambda x: x[2], reverse=True) # 按次数降序 + result.sort(key=lambda x: x[2], reverse=True) # 按次数降序 logger.debug(f"Selected nicknames for prompt: {result}") return result @@ -116,11 +117,10 @@ def format_nickname_prompt_injection(selected_nicknames: List[Tuple[str, str, in if user_name not in grouped_by_user: grouped_by_user[user_name] = [] # 添加引号以区分绰号 - grouped_by_user[user_name].append(f'“{nickname}”') + grouped_by_user[user_name].append(f"“{nickname}”") for user_name, nicknames in grouped_by_user.items(): nicknames_str = "、".join(nicknames) prompt_lines.append(f"{user_name},在本群有时被称为:{nicknames_str}") - return "\n".join(prompt_lines) + "\n" # 末尾加换行符 - + return "\n".join(prompt_lines) + "\n" # 末尾加换行符 diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index faa0ffd5..3d536edf 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -20,7 +20,11 @@ from src.heart_flow.sub_mind import SubMind from src.heart_flow.observation import Observation from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder import contextlib -from src.plugins.utils.chat_message_builder import num_new_messages_since, get_raw_msg_before_timestamp_with_chat, build_readable_messages +from src.plugins.utils.chat_message_builder import ( + num_new_messages_since, + get_raw_msg_before_timestamp_with_chat, + build_readable_messages, +) from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo from .heartFC_sender import HeartFCSender from src.plugins.chat.utils import process_llm_response @@ -28,7 +32,7 @@ from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.moods.moods import MoodManager from src.individuality.individuality import Individuality from src.plugins.person_info.relationship_manager import relationship_manager -from src.plugins.group_nickname.nickname_processor import add_to_nickname_queue # <--- 导入队列添加函数 +from src.plugins.group_nickname.nickname_processor import add_to_nickname_queue # <--- 导入队列添加函数 WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒 @@ -511,15 +515,15 @@ class HeartFChatting: if action == "text_reply": # 调用文本回复处理,它会返回 (bool, thinking_id) success, thinking_id = await handler(reasoning, emoji_query, cycle_timers) - return success, thinking_id # 直接返回结果 + return success, thinking_id # 直接返回结果 elif action == "emoji_reply": # 调用表情回复处理,它只返回 bool success = await handler(reasoning, emoji_query) - return success, "" # thinking_id 为空字符串 + return success, "" # thinking_id 为空字符串 else: # no_reply # 调用不回复处理,它只返回 bool success = await handler(reasoning, planner_start_db_time, cycle_timers) - return success, "" # thinking_id 为空字符串 + return success, "" # thinking_id 为空字符串 except HeartFCError as e: logger.error(f"{self.log_prefix} 处理{action}时出错: {e}") # 出错时也重置计数器 @@ -560,7 +564,7 @@ class HeartFChatting: if not thinking_id: raise PlannerError("无法创建思考消息") - reply = None # 初始化 reply + reply = None # 初始化 reply try: # 生成回复 with Timer("生成回复", cycle_timers): @@ -703,43 +707,43 @@ class HeartFChatting: reply: Bot 生成的回复内容列表。 """ if not global_config.ENABLE_NICKNAME_MAPPING: - return # 如果功能未开启,则直接返回 + return # 如果功能未开启,则直接返回 if not anchor_message or not anchor_message.chat_stream or not anchor_message.chat_stream.group_info: logger.debug(f"{self.log_prefix} Skipping nickname analysis: Not a group chat or invalid anchor.") - return # 仅在群聊中进行分析 + return # 仅在群聊中进行分析 try: # 1. 获取原始消息列表 - history_limit = 30 # 例如,获取最近 30 条消息 + history_limit = 30 # 例如,获取最近 30 条消息 history_messages = get_raw_msg_before_timestamp_with_chat( chat_id=anchor_message.chat_stream.stream_id, - timestamp=time.time(), # 获取当前时间点的历史 - limit=history_limit + timestamp=time.time(), # 获取当前时间点的历史 + limit=history_limit, ) # 格式化历史记录 chat_history_str = await build_readable_messages( messages=history_messages, replace_bot_name=True, # 在分析时也替换机器人名字,使其与 LLM 交互一致 - merge_messages=False, # 不合并,保留原始对话流 - timestamp_mode="relative", # 使用相对时间戳 - read_mark=0.0, # 不需要已读标记 - truncate=False # 获取完整内容进行分析 + merge_messages=False, # 不合并,保留原始对话流 + timestamp_mode="relative", # 使用相对时间戳 + read_mark=0.0, # 不需要已读标记 + truncate=False, # 获取完整内容进行分析 ) # 2. 获取 Bot 回复字符串 bot_reply_str = " ".join(reply) # 3. 获取群号 - group_id = str(anchor_message.chat_stream.group_info.group_id) # 确保是字符串 + group_id = str(anchor_message.chat_stream.group_info.group_id) # 确保是字符串 # 4. 获取当前上下文中涉及的用户 ID 及其已知名称 user_ids_in_history = set() for msg in history_messages: - sender_id = msg["user_info"].get('user_id') + sender_id = msg["user_info"].get("user_id") if sender_id: - user_ids_in_history.add(str(sender_id)) # 确保是字符串 + user_ids_in_history.add(str(sender_id)) # 确保是字符串 user_name_map = {} if user_ids_in_history: @@ -749,7 +753,7 @@ class HeartFChatting: except Exception as e: logger.error(f"Error getting person names: {e}", exc_info=True) - names_data = {} # 出错时置空 + names_data = {} # 出错时置空 print(f"\n\nnames_data:\n{names_data}\n\n") for user_id in user_ids_in_history: @@ -757,14 +761,21 @@ class HeartFChatting: user_name_map[user_id] = names_data[user_id] else: # 回退查找 nickname - latest_nickname = next((m.get('sender_nickname') for m in reversed(history_messages) if str(m.get('sender_id')) == user_id), None) + latest_nickname = next( + ( + m.get("sender_nickname") + for m in reversed(history_messages) + if str(m.get("sender_id")) == user_id + ), + None, + ) if latest_nickname: - user_name_map[user_id] = latest_nickname + user_name_map[user_id] = latest_nickname else: - user_name_map[user_id] = f"未知({user_id})" + user_name_map[user_id] = f"未知({user_id})" # 5. 添加到队列 - await add_to_nickname_queue(chat_history_str, bot_reply_str,platform, group_id, user_name_map) + await add_to_nickname_queue(chat_history_str, bot_reply_str, platform, group_id, user_name_map) logger.debug(f"{self.log_prefix} Triggered nickname analysis for group {group_id}.") except Exception as e: diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index db764385..3980b044 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -222,13 +222,12 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s user_ids_in_context = set() if message_list_before_now: for msg in message_list_before_now: - sender_id = msg["user_info"].get('user_id') + sender_id = msg["user_info"].get("user_id") if sender_id: user_ids_in_context.add(str(sender_id)) else: logger.warning("Variable 'message_list_before_now' not found for nickname injection in focus prompt.") - if user_ids_in_context: platform = chat_stream.platform # --- 调用批量获取群组绰号的方法 --- @@ -439,12 +438,13 @@ class PromptBuilder: user_ids_in_context = set() if message_list_before_now: for msg in message_list_before_now: - sender_id = msg["user_info"].get('user_id') + sender_id = msg["user_info"].get("user_id") if sender_id: user_ids_in_context.add(str(sender_id)) else: - logger.warning("Variable 'message_list_before_now' not found for nickname injection in focus prompt.") - + logger.warning( + "Variable 'message_list_before_now' not found for nickname injection in focus prompt." + ) if user_ids_in_context: platform = chat_stream.platform diff --git a/src/plugins/person_info/relationship_manager.py b/src/plugins/person_info/relationship_manager.py index 78bcb7c7..c7837dc5 100644 --- a/src/plugins/person_info/relationship_manager.py +++ b/src/plugins/person_info/relationship_manager.py @@ -96,19 +96,19 @@ class RelationshipManager: try: cursor = db.person_info.find( {"person_id": {"$in": person_ids}}, - {"_id": 0, "person_id": 1, "user_id": 1, "person_name": 1} # 只查询需要的字段 + {"_id": 0, "person_id": 1, "user_id": 1, "person_name": 1}, # 只查询需要的字段 ) for doc in cursor: - user_id_val = doc.get("user_id") # 获取原始值 - original_user_id = None # 初始化 + user_id_val = doc.get("user_id") # 获取原始值 + original_user_id = None # 初始化 - if isinstance(user_id_val, (int, float)): # 检查是否是数字类型 - original_user_id = str(user_id_val) # 直接转换为字符串 - elif isinstance(user_id_val, str): # 检查是否是字符串 - if "_" in user_id_val: # 如果包含下划线,则分割 + if isinstance(user_id_val, (int, float)): # 检查是否是数字类型 + original_user_id = str(user_id_val) # 直接转换为字符串 + elif isinstance(user_id_val, str): # 检查是否是字符串 + if "_" in user_id_val: # 如果包含下划线,则分割 original_user_id = user_id_val.split("_", 1)[-1] - else: # 如果不包含下划线,则直接使用该字符串 + else: # 如果不包含下划线,则直接使用该字符串 original_user_id = user_id_val # else: # 其他类型或 None,original_user_id 保持为 None @@ -127,7 +127,9 @@ class RelationshipManager: return names_map @staticmethod - async def get_users_group_nicknames(platform: str, user_ids: List[str], group_id: str) -> Dict[str, List[Dict[str, int]]]: + async def get_users_group_nicknames( + platform: str, user_ids: List[str], group_id: str + ) -> Dict[str, List[Dict[str, int]]]: """ 批量获取多个用户在指定群组的绰号信息。 @@ -144,23 +146,23 @@ class RelationshipManager: person_ids = [person_info_manager.get_person_id(platform, str(uid)) for uid in user_ids] nicknames_data = {} - group_id_str = str(group_id) # 确保 group_id 是字符串 + group_id_str = str(group_id) # 确保 group_id 是字符串 try: # 查询包含目标 person_id 的文档 cursor = db.person_info.find( {"person_id": {"$in": person_ids}}, - {"_id": 0, "person_id": 1, "person_name": 1, "group_nicknames": 1} # 查询所需字段 + {"_id": 0, "person_id": 1, "person_name": 1, "group_nicknames": 1}, # 查询所需字段 ) # 假设同步迭代可行 for doc in cursor: person_name = doc.get("person_name") if not person_name: - continue # 跳过没有 person_name 的用户 + continue # 跳过没有 person_name 的用户 - group_nicknames_list = doc.get("group_nicknames", []) # 获取 group_nicknames 数组 - target_group_nicknames = [] # 存储目标群组的绰号列表 + group_nicknames_list = doc.get("group_nicknames", []) # 获取 group_nicknames 数组 + target_group_nicknames = [] # 存储目标群组的绰号列表 # 遍历 group_nicknames 数组,查找匹配的 group_id for group_entry in group_nicknames_list: @@ -170,27 +172,33 @@ class RelationshipManager: nicknames_raw = group_entry.get("nicknames", []) if isinstance(nicknames_raw, list): target_group_nicknames = nicknames_raw - break # 找到匹配的 group_id 后即可退出内层循环 + break # 找到匹配的 group_id 后即可退出内层循环 # 如果找到了目标群组的绰号列表 if target_group_nicknames: - valid_nicknames_formatted = [] # 存储格式化后的绰号 + valid_nicknames_formatted = [] # 存储格式化后的绰号 for item in target_group_nicknames: # 校验每个绰号条目的格式 { "name": str, "count": int } - if isinstance(item, dict) and \ - isinstance(item.get("name"), str) and \ - isinstance(item.get("count"), int) and \ - item["count"] > 0: # 确保 count 是正整数 + if ( + isinstance(item, dict) + and isinstance(item.get("name"), str) + and isinstance(item.get("count"), int) + and item["count"] > 0 + ): # 确保 count 是正整数 # --- 格式转换:从 { "name": "xxx", "count": y } 转为 { "xxx": y } --- valid_nicknames_formatted.append({item["name"]: item["count"]}) # --- 结束格式转换 --- else: - logger.warning(f"数据库中用户 {person_name} 群组 {group_id_str} 的绰号格式无效或 count <= 0: {item}") + logger.warning( + f"数据库中用户 {person_name} 群组 {group_id_str} 的绰号格式无效或 count <= 0: {item}" + ) - if valid_nicknames_formatted: # 如果存在有效的、格式化后的绰号 - nicknames_data[person_name] = valid_nicknames_formatted # 使用 person_name 作为 key + if valid_nicknames_formatted: # 如果存在有效的、格式化后的绰号 + nicknames_data[person_name] = valid_nicknames_formatted # 使用 person_name 作为 key - logger.debug(f"批量获取群组 {group_id_str} 中 {len(user_ids)} 个用户的绰号,找到 {len(nicknames_data)} 个用户的数据。") + logger.debug( + f"批量获取群组 {group_id_str} 中 {len(user_ids)} 个用户的绰号,找到 {len(nicknames_data)} 个用户的数据。" + ) except AttributeError as e: logger.error(f"访问数据库时出错: {e}。请检查 common/database.py 和集合名称 'person_info'。")