🤖 自动格式化代码 [skip ci]

pull/914/head
github-actions[bot] 2025-05-01 15:09:56 +00:00
parent 6e66d66b1f
commit 635ead2b6a
8 changed files with 161 additions and 126 deletions

9
bot.py
View File

@ -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("已注册绰号处理线程的退出处理程序。")
# 创建事件循环

View File

@ -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):
# 机器人基础配置

View File

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

View File

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

View File

@ -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" # 末尾加换行符

View File

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

View File

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

View File

@ -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: # 其他类型或 Noneoriginal_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'")