mirror of https://github.com/Mai-with-u/MaiBot.git
🤖 自动格式化代码 [skip ci]
parent
6e66d66b1f
commit
635ead2b6a
9
bot.py
9
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("已注册绰号处理线程的退出处理程序。")
|
||||
|
||||
# 创建事件循环
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
# 机器人基础配置
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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" # 末尾加换行符
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'。")
|
||||
|
|
|
|||
Loading…
Reference in New Issue