feat:整合normal和focus聊天

This commit is contained in:
SengokuCola
2025-07-12 01:24:13 +08:00
parent a0efb89d98
commit 4255e64d35
18 changed files with 1153 additions and 1021 deletions

View File

@@ -5,11 +5,9 @@ MaiBot模块系统
from src.chat.message_receive.chat_stream import get_chat_manager
from src.chat.emoji_system.emoji_manager import get_emoji_manager
from src.chat.normal_chat.willing.willing_manager import get_willing_manager
# 导出主要组件供外部使用
__all__ = [
"get_chat_manager",
"get_emoji_manager",
"get_willing_manager",
]

View File

@@ -1,9 +1,10 @@
import asyncio
import contextlib
import time
import traceback
from collections import deque
from typing import List, Optional, Dict, Any, Deque, Callable, Awaitable
from typing import Optional, Deque, Callable, Awaitable
from sqlalchemy import False_
from src.chat.message_receive.chat_stream import get_chat_manager
from rich.traceback import install
from src.chat.utils.prompt_builder import global_prompt_manager
@@ -16,6 +17,16 @@ from src.chat.planner_actions.action_manager import ActionManager
from src.config.config import global_config
from src.person_info.relationship_builder_manager import relationship_builder_manager
from src.chat.focus_chat.hfc_utils import CycleDetail
from random import random
from src.chat.focus_chat.hfc_utils import create_thinking_message_from_dict, add_messages_to_manager,get_recent_message_stats,cleanup_thinking_message_by_id
from src.person_info.person_info import get_person_info_manager
from src.plugin_system.apis import generator_api
from ..message_receive.message import MessageThinking
from src.chat.message_receive.normal_message_sender import message_manager
from src.chat.willing.willing_manager import get_willing_manager
from .priority_manager import PriorityManager
from src.chat.utils.chat_message_builder import get_raw_msg_by_timestamp_with_chat_inclusive
ERROR_LOOP_INFO = {
@@ -34,6 +45,17 @@ ERROR_LOOP_INFO = {
},
}
NO_ACTION = {
"action_result": {
"action_type": "no_action",
"action_data": {},
"reasoning": "规划器初始化默认",
"is_parallel": True,
},
"chat_context": "",
"action_prompt": "",
}
install(extra_lines=3)
# 注释:原来的动作修改超时常量已移除,因为改为顺序执行
@@ -51,7 +73,6 @@ class HeartFChatting:
def __init__(
self,
chat_id: str,
on_stop_focus_chat: Optional[Callable[[], Awaitable[None]]] = None,
):
"""
HeartFChatting 初始化函数
@@ -68,6 +89,10 @@ class HeartFChatting:
self.relationship_builder = relationship_builder_manager.get_or_create_builder(self.stream_id)
self.loop_mode = "normal"
self.recent_replies = []
# 新增:消息计数器和疲惫阈值
self._message_count = 0 # 发送的消息计数
# 基于exit_focus_threshold动态计算疲惫阈值
@@ -90,12 +115,33 @@ class HeartFChatting:
self._cycle_history: Deque[CycleDetail] = deque(maxlen=10) # 保留最近10个循环的信息
self._current_cycle_detail: Optional[CycleDetail] = None
# 存储回调函数
self.on_stop_focus_chat = on_stop_focus_chat
self.reply_timeout_count = 0
self.plan_timeout_count = 0
self.last_read_time = time.time()-1
self.willing_amplifier = 1
self.action_type: Optional[str] = None # 当前动作类型
self.is_parallel_action: bool = False # 是否是可并行动作
self._chat_task: Optional[asyncio.Task] = None
self._priority_chat_task: Optional[asyncio.Task] = None # for priority mode consumer
self.reply_mode = self.chat_stream.context.get_priority_mode()
if self.reply_mode == "priority":
self.priority_manager = PriorityManager(
normal_queue_max_size=5,
)
else:
self.priority_manager = None
self.willing_manager = get_willing_manager()
logger.info(
f"{self.log_prefix} HeartFChatting 初始化完成,消息疲惫阈值: {self._message_threshold}基于exit_focus_threshold={global_config.chat.exit_focus_threshold}计算仅在auto模式下生效"
)
@@ -172,14 +218,41 @@ class HeartFChatting:
async def _focus_mode_loopbody(self):
logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次循环")
async def _loopbody(self):
if self.loop_mode == "focus":
logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次观察")
return await self._observe()
elif self.loop_mode == "normal":
now = time.time()
new_messages_data = get_raw_msg_by_timestamp_with_chat_inclusive(
chat_id=self.stream_id, timestamp_start=self.last_read_time, timestamp_end=now, limit_mode="earliest"
)
if new_messages_data:
self.last_read_time = now
for msg_data in new_messages_data:
try:
self.adjust_reply_frequency()
logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次循环")
await self.normal_response(msg_data)
# TODO: 这个地方可能导致阻塞,需要优化
return True
except Exception as e:
logger.error(f"[{self.log_prefix}] 处理消息时出错: {e} {traceback.format_exc()}")
else:
await asyncio.sleep(0.1)
return True
async def _observe(self,message_data:dict = None):
# 创建新的循环信息
cycle_timers, thinking_id = self.start_cycle()
# 执行规划和处理阶段
try:
await create_thinking_message_from_dict(message_data,self.chat_stream,thinking_id)
async with global_prompt_manager.async_message_scope(
self.chat_stream.context.get_template_name()
):
@@ -191,25 +264,123 @@ class HeartFChatting:
# 第一步:动作修改
with Timer("动作修改", cycle_timers):
try:
if self.loop_mode == "focus":
await self.action_modifier.modify_actions(
loop_info=self.loop_info,
mode="focus",
)
elif self.loop_mode == "normal":
await self.action_modifier.modify_actions(mode="normal")
available_actions = self.action_manager.get_using_actions_for_mode("normal")
except Exception as e:
logger.error(f"{self.log_prefix} 动作修改失败: {e}")
#如果normal开始一个回复生成进程先准备好回复其实是和planer同时进行的
if self.loop_mode == "normal":
gen_task = asyncio.create_task(self._generate_normal_response(message_data, available_actions))
with Timer("规划器", cycle_timers):
plan_result = await self.action_planner.plan()
if self.loop_mode == "focus":
if self.action_modifier.should_skip_planning_for_no_reply():
logger.info(f"[{self.log_prefix}] 没有可用动作,跳过规划")
action_type = "no_reply"
else:
plan_result = await self.action_planner.plan(mode="focus")
elif self.loop_mode == "normal":
if self.action_modifier.should_skip_planning_for_no_action():
logger.info(f"[{self.log_prefix}] 没有可用动作,跳过规划")
action_type = "no_action"
else:
plan_result = await self.action_planner.plan(mode="normal")
action_result = plan_result.get("action_result", {})
action_type, action_data, reasoning = (
action_type, action_data, reasoning, is_parallel = (
action_result.get("action_type", "error"),
action_result.get("action_data", {}),
action_result.get("reasoning", "未提供理由"),
action_result.get("is_parallel", True),
)
action_data["loop_start_time"] = loop_start_time
if self.loop_mode == "normal":
if action_type == "no_action":
logger.info(f"[{self.log_prefix}] {global_config.bot.nickname} 决定进行回复")
elif is_parallel:
logger.info(
f"[{self.log_prefix}] {global_config.bot.nickname} 决定进行回复, 同时执行{action_type}动作"
)
else:
logger.info(f"[{self.log_prefix}] {global_config.bot.nickname} 决定执行{action_type}动作")
if action_type == "no_action":
gather_timeout = global_config.chat.thinking_timeout
results = await asyncio.wait_for(
asyncio.gather(gen_task, return_exceptions=True),
timeout=gather_timeout,
)
response_set = results[0]
if response_set:
content = " ".join([item[1] for item in response_set if item[0] == "text"])
if not response_set or (
action_type not in ["no_action"] and not is_parallel
):
if not response_set:
logger.warning(f"[{self.log_prefix}] 模型未生成回复内容")
elif action_type not in ["no_action"] and not is_parallel:
logger.info(
f"[{self.log_prefix}] {global_config.bot.nickname} 原本想要回复:{content},但选择执行{self.action_type},不发表回复"
)
# 如果模型未生成回复,移除思考消息
await cleanup_thinking_message_by_id(self.chat_stream.stream_id,thinking_id,self.log_prefix)
return False
logger.info(f"[{self.log_prefix}] {global_config.bot.nickname} 决定的回复内容: {content}")
# 提取回复文本
reply_texts = [item[1] for item in response_set if item[0] == "text"]
if not reply_texts:
logger.info(f"[{self.log_prefix}] 回复内容中没有文本,不发送消息")
await cleanup_thinking_message_by_id(self.chat_stream.stream_id,thinking_id,self.log_prefix)
return False
# 发送回复 (不再需要传入 chat)
first_bot_msg = await add_messages_to_manager(message_data, reply_texts, thinking_id,self.chat_stream.stream_id)
# 检查 first_bot_msg 是否为 None (例如思考消息已被移除的情况)
if first_bot_msg:
# 消息段已在接收消息时更新,这里不需要额外处理
# 记录回复信息到最近回复列表中
reply_info = {
"time": time.time(),
"user_message": message_data.get("processed_plain_text"),
"user_info": {
"user_id": message_data.get("user_id"),
"user_nickname": message_data.get("user_nickname"),
},
"response": response_set,
"is_reference_reply": message_data.get("reply") is not None, # 判断是否为引用回复
}
self.recent_replies.append(reply_info)
# 保持最近回复历史在限定数量内
if len(self.recent_replies) > 10:
self.recent_replies = self.recent_replies[-10 :]
return response_set if response_set else False
else:
# 动作执行计时
with Timer("动作执行", cycle_timers):
success, reply_text, command = await self._handle_action(
@@ -236,52 +407,30 @@ class HeartFChatting:
self.end_cycle(loop_info,cycle_timers)
self.print_cycle_info(cycle_timers)
await asyncio.sleep(global_config.focus_chat.think_interval)
if self.loop_mode == "normal":
await self.willing_manager.after_generate_reply_handle(message_data.get("message_id"))
return True
except asyncio.CancelledError:
logger.info(f"{self.log_prefix} focus循环任务被取消")
return False
except Exception as e:
logger.error(f"{self.log_prefix} 循环处理时出错: {e}")
logger.error(traceback.format_exc())
# 如果_current_cycle_detail存在但未完成为其设置错误状态
if self._current_cycle_detail and not hasattr(self._current_cycle_detail, "end_time"):
error_loop_info = ERROR_LOOP_INFO
try:
self._current_cycle_detail.set_loop_info(error_loop_info)
self._current_cycle_detail.complete_cycle()
except Exception as inner_e:
logger.error(f"{self.log_prefix} 设置错误状态时出错: {inner_e}")
await asyncio.sleep(1) # 出错后等待一秒再继续\
return False
async def _main_chat_loop(self):
"""主循环,持续进行计划并可能回复消息,直到被外部取消。"""
try:
loop_mode = "focus"
loop_mode_loopbody = self._focus_mode_loopbody
while self.running: # 主循环
success = await loop_mode_loopbody()
success = await self._loopbody()
if not success:
break
logger.info(f"{self.log_prefix} 麦麦已强制离开 {loop_mode} 聊天模式")
logger.info(f"{self.log_prefix} 麦麦已强制离开聊天")
except asyncio.CancelledError:
# 设置了关闭标志位后被取消是正常流程
logger.info(f"{self.log_prefix} 麦麦已强制离开 {loop_mode} 聊天模式")
except Exception as e:
logger.error(f"{self.log_prefix} 麦麦 {loop_mode} 聊天模式意外错误: {e}")
logger.info(f"{self.log_prefix} 麦麦已关闭聊天")
except Exception:
logger.error(f"{self.log_prefix} 麦麦聊天意外错误")
print(traceback.format_exc())
# 理论上不能到这里
logger.error(f"{self.log_prefix} 麦麦聊天意外错误,结束了聊天循环")
async def _handle_action(
self,
@@ -376,8 +525,6 @@ class HeartFChatting:
return command
return ""
def _get_current_fatigue_threshold(self) -> int:
"""动态获取当前的疲惫阈值基于exit_focus_threshold配置
@@ -427,3 +574,226 @@ class HeartFChatting:
logger.info(f"{self.log_prefix} HeartFChatting关闭完成")
def adjust_reply_frequency(self):
"""
根据预设规则动态调整回复意愿willing_amplifier
- 评估周期10分钟
- 目标频率:由 global_config.chat.talk_frequency 定义(例如 1条/分钟)
- 调整逻辑:
- 0条回复 -> 5.0x 意愿
- 达到目标回复数 -> 1.0x 意愿(基准)
- 达到目标2倍回复数 -> 0.2x 意愿
- 中间值线性变化
- 增益抑制如果最近5分钟回复过快则不增加意愿。
"""
# --- 1. 定义参数 ---
evaluation_minutes = 10.0
target_replies_per_min = global_config.chat.get_current_talk_frequency(
self.stream_id
) # 目标频率e.g. 1条/分钟
target_replies_in_window = target_replies_per_min * evaluation_minutes # 10分钟内的目标回复数
if target_replies_in_window <= 0:
logger.debug(f"[{self.log_prefix}] 目标回复频率为0或负数不调整意愿放大器。")
return
# --- 2. 获取近期统计数据 ---
stats_10_min = get_recent_message_stats(minutes=evaluation_minutes, chat_id=self.stream_id)
bot_reply_count_10_min = stats_10_min["bot_reply_count"]
# --- 3. 计算新的意愿放大器 (willing_amplifier) ---
# 基于回复数在 [0, target*2] 区间内进行分段线性映射
if bot_reply_count_10_min <= target_replies_in_window:
# 在 [0, 目标数] 区间,意愿从 5.0 线性下降到 1.0
new_amplifier = 5.0 + (bot_reply_count_10_min - 0) * (1.0 - 5.0) / (target_replies_in_window - 0)
elif bot_reply_count_10_min <= target_replies_in_window * 2:
# 在 [目标数, 目标数*2] 区间,意愿从 1.0 线性下降到 0.2
over_target_cap = target_replies_in_window * 2
new_amplifier = 1.0 + (bot_reply_count_10_min - target_replies_in_window) * (0.2 - 1.0) / (
over_target_cap - target_replies_in_window
)
else:
# 超过目标数2倍直接设为最小值
new_amplifier = 0.2
# --- 4. 检查是否需要抑制增益 ---
# "如果邻近5分钟内回复数量 > 频率/2就不再进行增益"
suppress_gain = False
if new_amplifier > self.willing_amplifier: # 仅在计算结果为增益时检查
suppression_minutes = 5.0
# 5分钟内目标回复数的一半
suppression_threshold = (target_replies_per_min / 2) * suppression_minutes # e.g., (1/2)*5 = 2.5
stats_5_min = get_recent_message_stats(minutes=suppression_minutes, chat_id=self.stream_id)
bot_reply_count_5_min = stats_5_min["bot_reply_count"]
if bot_reply_count_5_min > suppression_threshold:
suppress_gain = True
# --- 5. 更新意愿放大器 ---
if suppress_gain:
logger.debug(
f"[{self.log_prefix}] 回复增益被抑制。最近5分钟内回复数 ({bot_reply_count_5_min}) "
f"> 阈值 ({suppression_threshold:.1f})。意愿放大器保持在 {self.willing_amplifier:.2f}"
)
# 不做任何改动
else:
# 限制最终值在 [0.2, 5.0] 范围内
self.willing_amplifier = max(0.2, min(5.0, new_amplifier))
logger.debug(
f"[{self.log_prefix}] 调整回复意愿。10分钟内回复: {bot_reply_count_10_min} (目标: {target_replies_in_window:.0f}) -> "
f"意愿放大器更新为: {self.willing_amplifier:.2f}"
)
async def normal_response(self, message_data: dict) -> None:
"""
处理接收到的消息。
"兴趣"模式下,判断是否回复并生成内容。
"""
is_mentioned = message_data.get("is_mentioned", False)
interested_rate = message_data.get("interest_rate", 0.0) * self.willing_amplifier
reply_probability = (
1.0 if is_mentioned and global_config.normal_chat.mentioned_bot_inevitable_reply else 0.0
) # 如果被提及且开启了提及必回复则基础概率为1否则需要意愿判断
# 意愿管理器设置当前message信息
self.willing_manager.setup(message_data, self.chat_stream)
# 获取回复概率
# 仅在未被提及或基础概率不为1时查询意愿概率
if reply_probability < 1: # 简化逻辑,如果未提及 (reply_probability 为 0),则获取意愿概率
# is_willing = True
reply_probability = await self.willing_manager.get_reply_probability(message_data.get("message_id"))
additional_config = message_data.get("additional_config", {})
if additional_config and "maimcore_reply_probability_gain" in additional_config:
reply_probability += additional_config["maimcore_reply_probability_gain"]
reply_probability = min(max(reply_probability, 0), 1) # 确保概率在 0-1 之间
# 处理表情包
if message_data.get("is_emoji") or message_data.get("is_picid"):
reply_probability = 0
# 应用疲劳期回复频率调整
fatigue_multiplier = self._get_fatigue_reply_multiplier()
original_probability = reply_probability
reply_probability *= fatigue_multiplier
# 如果应用了疲劳调整,记录日志
if fatigue_multiplier < 1.0:
logger.info(
f"[{self.log_prefix}] 疲劳期回复频率调整: {original_probability * 100:.1f}% -> {reply_probability * 100:.1f}% (系数: {fatigue_multiplier:.2f})"
)
# 打印消息信息
mes_name = self.chat_stream.group_info.group_name if self.chat_stream.group_info else "私聊"
if reply_probability > 0.1:
logger.info(
f"[{mes_name}]"
f"{message_data.get('user_nickname')}:"
f"{message_data.get('processed_plain_text')}[兴趣:{interested_rate:.2f}][回复概率:{reply_probability * 100:.1f}%]"
)
if random() < reply_probability:
await self.willing_manager.before_generate_reply_handle(message_data.get("message_id"))
await self._observe(message_data = message_data)
# 意愿管理器注销当前message信息 (无论是否回复,只要处理过就删除)
self.willing_manager.delete(message_data.get("message_id"))
return True
async def _generate_normal_response(
self, message_data: dict, available_actions: Optional[list]
) -> Optional[list]:
"""生成普通回复"""
try:
person_info_manager = get_person_info_manager()
person_id = person_info_manager.get_person_id(
message_data.get("chat_info_platform"), message_data.get("user_id")
)
person_name = await person_info_manager.get_value(person_id, "person_name")
reply_to_str = f"{person_name}:{message_data.get('processed_plain_text')}"
success, reply_set = await generator_api.generate_reply(
chat_stream=self.chat_stream,
reply_to=reply_to_str,
available_actions=available_actions,
enable_tool=global_config.tool.enable_in_normal_chat,
request_type="normal.replyer",
)
if not success or not reply_set:
logger.info(f"{message_data.get('processed_plain_text')} 的回复生成失败")
return None
return reply_set
except Exception as e:
logger.error(f"[{self.log_prefix}] 回复生成出现错误:{str(e)} {traceback.format_exc()}")
return None
def _get_fatigue_reply_multiplier(self) -> float:
"""获取疲劳期回复频率调整系数
Returns:
float: 回复频率调整系数范围0.5-1.0
"""
if not self.get_cooldown_progress_callback:
return 1.0 # 没有冷却进度回调,返回正常系数
try:
cooldown_progress = self.get_cooldown_progress_callback()
if cooldown_progress >= 1.0:
return 1.0 # 冷却完成,正常回复频率
# 疲劳期间从0.5逐渐恢复到1.0
# progress=0时系数为0.5progress=1时系数为1.0
multiplier = 0.2 + (0.8 * cooldown_progress)
return multiplier
except Exception as e:
logger.warning(f"[{self.log_prefix}] 获取疲劳调整系数时出错: {e}")
return 1.0 # 出错时返回正常系数
# async def _check_should_switch_to_focus(self) -> bool:
# """
# 检查是否满足切换到focus模式的条件
# Returns:
# bool: 是否应该切换到focus模式
# """
# # 检查思考消息堆积情况
# container = await message_manager.get_container(self.stream_id)
# if container:
# thinking_count = sum(1 for msg in container.messages if isinstance(msg, MessageThinking))
# if thinking_count >= 4 * global_config.chat.auto_focus_threshold: # 如果堆积超过阈值条思考消息
# logger.debug(f"[{self.stream_name}] 检测到思考消息堆积({thinking_count}条)切换到focus模式")
# return True
# if not self.recent_replies:
# return False
# current_time = time.time()
# time_threshold = 120 / global_config.chat.auto_focus_threshold
# reply_threshold = 6 * global_config.chat.auto_focus_threshold
# one_minute_ago = current_time - time_threshold
# # 统计指定时间内的回复数量
# recent_reply_count = sum(1 for reply in self.recent_replies if reply["time"] > one_minute_ago)
# should_switch = recent_reply_count > reply_threshold
# if should_switch:
# logger.debug(
# f"[{self.stream_name}] 检测到{time_threshold:.0f}秒内回复数量({recent_reply_count})大于{reply_threshold}满足切换到focus模式条件"
# )
# return should_switch

View File

@@ -1,11 +1,19 @@
import time
from typing import Optional
from src.chat.message_receive.message import MessageRecv, BaseMessageInfo
from src.chat.message_receive.chat_stream import ChatStream
from src.chat.message_receive.message import UserInfo
from src.common.logger import get_logger
import json
from typing import Dict, Any
from src.config.config import global_config
from src.chat.message_receive.message import MessageThinking
from src.chat.message_receive.normal_message_sender import message_manager
from typing import List
from maim_message import Seg
from src.common.message_repository import count_messages
from ..message_receive.message import MessageSending, MessageSet, message_from_db_dict
from src.chat.message_receive.chat_stream import get_chat_manager
logger = get_logger(__name__)
@@ -113,3 +121,129 @@ def parse_thinking_id_to_timestamp(thinking_id: str) -> float:
ts_str = thinking_id[3:]
return float(ts_str)
async def create_thinking_message_from_dict(message_data: dict, chat_stream: ChatStream, thinking_id: str) -> str:
"""创建思考消息"""
bot_user_info = UserInfo(
user_id=global_config.bot.qq_account,
user_nickname=global_config.bot.nickname,
platform=message_data.get("chat_info_platform"),
)
thinking_message = MessageThinking(
message_id=thinking_id,
chat_stream=chat_stream,
bot_user_info=bot_user_info,
reply=None,
thinking_start_time=time.time(),
timestamp=time.time(),
)
await message_manager.add_message(thinking_message)
return thinking_id
async def cleanup_thinking_message_by_id(chat_id: str, thinking_id: str, log_prefix: str):
"""根据ID清理思考消息"""
try:
container = await message_manager.get_container(chat_id)
if container:
for msg in container.messages[:]:
if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id:
container.messages.remove(msg)
logger.info(f"{log_prefix}已清理思考消息 {thinking_id}")
break
except Exception as e:
logger.error(f"{log_prefix} 清理思考消息 {thinking_id} 时出错: {e}")
async def add_messages_to_manager(
message_data: dict, response_set: List[str], thinking_id, chat_id
) -> Optional[MessageSending]:
"""发送回复消息"""
chat_stream = get_chat_manager().get_stream(chat_id)
container = await message_manager.get_container(chat_id) # 使用 self.stream_id
thinking_message = None
for msg in container.messages[:]:
# print(msg)
if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id:
thinking_message = msg
container.messages.remove(msg)
break
if not thinking_message:
logger.warning(f"[{chat_id}] 未找到对应的思考消息 {thinking_id},可能已超时被移除")
return None
thinking_start_time = thinking_message.thinking_start_time
message_set = MessageSet(chat_stream, thinking_id) # 使用 self.chat_stream
sender_info = UserInfo(
user_id=message_data.get("user_id"),
user_nickname=message_data.get("user_nickname"),
platform=message_data.get("chat_info_platform"),
)
reply = message_from_db_dict(message_data)
mark_head = False
first_bot_msg = None
for msg in response_set:
if global_config.debug.debug_show_chat_mode:
msg += ""
message_segment = Seg(type="text", data=msg)
bot_message = MessageSending(
message_id=thinking_id,
chat_stream=chat_stream, # 使用 self.chat_stream
bot_user_info=UserInfo(
user_id=global_config.bot.qq_account,
user_nickname=global_config.bot.nickname,
platform=message_data.get("chat_info_platform"),
),
sender_info=sender_info,
message_segment=message_segment,
reply=reply,
is_head=not mark_head,
is_emoji=False,
thinking_start_time=thinking_start_time,
apply_set_reply_logic=True,
)
if not mark_head:
mark_head = True
first_bot_msg = bot_message
message_set.add_message(bot_message)
await message_manager.add_message(message_set)
return first_bot_msg
def get_recent_message_stats(minutes: int = 30, chat_id: str = None) -> dict:
"""
Args:
minutes (int): 检索的分钟数默认30分钟
chat_id (str, optional): 指定的chat_id仅统计该chat下的消息。为None时统计全部。
Returns:
dict: {"bot_reply_count": int, "total_message_count": int}
"""
now = time.time()
start_time = now - minutes * 60
bot_id = global_config.bot.qq_account
filter_base = {"time": {"$gte": start_time}}
if chat_id is not None:
filter_base["chat_id"] = chat_id
# 总消息数
total_message_count = count_messages(filter_base)
# bot自身回复数
bot_filter = filter_base.copy()
bot_filter["user_id"] = bot_id
bot_reply_count = count_messages(bot_filter)
return {"bot_reply_count": bot_reply_count, "total_message_count": total_message_count}

View File

@@ -2,7 +2,7 @@ import time
import heapq
import math
import json
from typing import List, Dict, Optional
from typing import List, Optional
from src.common.logger import get_logger

View File

@@ -1,13 +0,0 @@
import enum
class ChatState(enum.Enum):
ABSENT = "没在看群"
NORMAL = "随便水群"
FOCUSED = "认真水群"
class ChatStateInfo:
def __init__(self):
self.chat_status: ChatState = ChatState.NORMAL
self.current_state_time = 120

View File

@@ -1,13 +1,11 @@
import asyncio
import time
from typing import Optional, List, Dict, Tuple
from typing import Optional, List, Tuple
import traceback
from src.common.logger import get_logger
from src.chat.message_receive.message import MessageRecv
from src.chat.message_receive.chat_stream import get_chat_manager
from src.chat.focus_chat.heartFC_chat import HeartFChatting
from src.chat.normal_chat.normal_chat import NormalChat
from src.chat.heart_flow.chat_state_info import ChatState, ChatStateInfo
from src.chat.utils.utils import get_chat_type_and_target_info
from src.config.config import global_config
from rich.traceback import install
@@ -31,11 +29,6 @@ class SubHeartflow:
self.subheartflow_id = subheartflow_id
self.chat_id = subheartflow_id
# 这个聊天流的状态
self.chat_state: ChatStateInfo = ChatStateInfo()
self.chat_state_changed_time: float = time.time()
self.chat_state_last_time: float = 0
self.history_chat_state: List[Tuple[ChatState, float]] = []
self.is_group_chat, self.chat_target_info = get_chat_type_and_target_info(self.chat_id)
self.log_prefix = get_chat_manager().get_stream_name(self.subheartflow_id) or self.subheartflow_id
@@ -47,125 +40,14 @@ class SubHeartflow:
# CHAT模式激活 随便水群 FOCUS模式激活 认真水群
self.heart_fc_instance: Optional[HeartFChatting] = HeartFChatting(
chat_id=self.subheartflow_id,
on_stop_focus_chat=self._handle_stop_focus_chat_request,
) # 该sub_heartflow的HeartFChatting实例
self.normal_chat_instance: Optional[NormalChat] = NormalChat(
chat_stream=get_chat_manager().get_stream(self.chat_id),
on_switch_to_focus_callback=self._handle_switch_to_focus_request,
get_cooldown_progress_callback=self.get_cooldown_progress,
) # 该sub_heartflow的NormalChat实例
async def initialize(self):
"""异步初始化方法,创建兴趣流并确定聊天类型"""
await self.heart_fc_instance.start()
# 根据配置决定初始状态
if not self.is_group_chat:
logger.debug(f"{self.log_prefix} 检测到是私聊,将直接尝试进入 FOCUSED 状态。")
await self.change_chat_state(ChatState.FOCUSED)
elif global_config.chat.chat_mode == "focus":
logger.debug(f"{self.log_prefix} 配置为 focus 模式,将直接尝试进入 FOCUSED 状态。")
await self.change_chat_state(ChatState.FOCUSED)
else: # "auto" 或其他模式保持原有逻辑或默认为 NORMAL
logger.debug(f"{self.log_prefix} 配置为 auto 或其他模式,将尝试进入 NORMAL 状态。")
await self.change_chat_state(ChatState.NORMAL)
def update_last_chat_state_time(self):
self.chat_state_last_time = time.time() - self.chat_state_changed_time
async def _stop_normal_chat(self):
"""
停止 NormalChat 实例
切出 CHAT 状态时使用
"""
if self.normal_chat_instance:
logger.info(f"{self.log_prefix} 离开normal模式")
try:
logger.debug(f"{self.log_prefix} 开始调用 stop_chat()")
# 使用更短的超时时间,强制快速停止
await asyncio.wait_for(self.normal_chat_instance.stop_chat(), timeout=3.0)
logger.debug(f"{self.log_prefix} stop_chat() 调用完成")
except Exception as e:
logger.error(f"{self.log_prefix} 停止 NormalChat 监控任务时出错: {e}")
# 出错时也要清理实例,避免状态不一致
self.normal_chat_instance = None
finally:
# 确保实例被清理
if self.normal_chat_instance:
logger.warning(f"{self.log_prefix} 强制清理 NormalChat 实例")
self.normal_chat_instance = None
logger.debug(f"{self.log_prefix} _stop_normal_chat 完成")
else:
logger.info(f"{self.log_prefix} 没有normal聊天实例无需停止normal聊天")
async def _start_normal_chat(self) -> bool:
"""
启动 NormalChat 实例,并进行异步初始化。
进入 CHAT 状态时使用。
确保 HeartFChatting 已停止。
"""
await self._stop_heart_fc_chat() # 确保 专注聊天已停止
try:
# 获取聊天流并创建 NormalChat 实例 (同步部分)
chat_stream = get_chat_manager().get_stream(self.chat_id)
# 在 NormalChat 实例尚未创建时,创建新实例
if not self.normal_chat_instance:
# 提供回调函数用于接收需要切换到focus模式的通知
self.normal_chat_instance = NormalChat(
chat_stream=chat_stream,
on_switch_to_focus_callback=self._handle_switch_to_focus_request,
get_cooldown_progress_callback=self.get_cooldown_progress,
)
logger.info(f"[{self.log_prefix}] 开始普通聊天")
await self.normal_chat_instance.start_chat() # start_chat now ensures init is called again if needed
return True
except Exception as e:
logger.error(f"[{self.log_prefix}] 启动 NormalChat 或其初始化时出错: {e}")
logger.error(traceback.format_exc())
self.normal_chat_instance = None # 启动/初始化失败,清理实例
return False
async def _handle_switch_to_focus_request(self) -> bool:
"""
处理来自NormalChat的切换到focus模式的请求
Args:
stream_id: 请求切换的stream_id
Returns:
bool: 切换成功返回True失败返回False
"""
logger.info(f"{self.log_prefix} 收到NormalChat请求切换到focus模式")
# 检查是否在focus冷却期内
if self.is_in_focus_cooldown():
logger.info(f"{self.log_prefix} 正在focus冷却期内忽略切换到focus模式的请求")
return False
# 切换到focus模式
current_state = self.chat_state.chat_status
if current_state == ChatState.NORMAL:
await self.change_chat_state(ChatState.FOCUSED)
logger.info(f"{self.log_prefix} 已根据NormalChat请求从NORMAL切换到FOCUSED状态")
return True
else:
logger.warning(f"{self.log_prefix} 当前状态为{current_state.value}无法切换到FOCUSED状态")
return False
async def _handle_stop_focus_chat_request(self) -> None:
"""
处理来自HeartFChatting的停止focus模式的请求
当收到stop_focus_chat命令时被调用
"""
logger.info(f"{self.log_prefix} 收到HeartFChatting请求停止focus模式")
# 切换到normal模式
current_state = self.chat_state.chat_status
if current_state == ChatState.FOCUSED:
await self.change_chat_state(ChatState.NORMAL)
logger.info(f"{self.log_prefix} 已根据HeartFChatting请求从FOCUSED切换到NORMAL状态")
else:
logger.warning(f"{self.log_prefix} 当前状态为{current_state.value}无法切换到NORMAL状态")
async def _stop_heart_fc_chat(self):
"""停止并清理 HeartFChatting 实例"""
@@ -205,63 +87,6 @@ class SubHeartflow:
logger.error(traceback.format_exc())
return False
async def change_chat_state(self, new_state: ChatState) -> None:
"""
改变聊天状态。
如果转换到CHAT或FOCUSED状态时超过限制会保持当前状态。
"""
current_state = self.chat_state.chat_status
state_changed = False
if new_state == ChatState.NORMAL:
if self.normal_chat_instance.running:
logger.info(f"{self.log_prefix} 当前状态已经为normal")
return
else:
if await self._start_normal_chat():
logger.debug(f"{self.log_prefix} 成功进入或保持 NormalChat 状态。")
state_changed = True
else:
logger.error(f"{self.log_prefix} 启动 NormalChat 失败,无法进入 CHAT 状态。")
return
elif new_state == ChatState.FOCUSED:
if self.heart_fc_instance.running:
logger.info(f"{self.log_prefix} 当前状态已经为focused")
return
if await self._start_heart_fc_chat():
logger.debug(f"{self.log_prefix} 成功进入或保持 HeartFChatting 状态。")
state_changed = True
else:
logger.error(f"{self.log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。")
# 启动失败时,保持当前状态
return
# --- 记录focus模式退出时间 ---
if state_changed and current_state == ChatState.FOCUSED and new_state != ChatState.FOCUSED:
self.last_focus_exit_time = time.time()
logger.debug(f"{self.log_prefix} 记录focus模式退出时间: {self.last_focus_exit_time}")
# --- 更新状态和最后活动时间 ---
if state_changed:
self.update_last_chat_state_time()
self.history_chat_state.append((current_state, self.chat_state_last_time))
self.chat_state.chat_status = new_state
self.chat_state_last_time = 0
self.chat_state_changed_time = time.time()
else:
logger.debug(
f"{self.log_prefix} 尝试将状态从 {current_state.value} 变为 {new_state.value},但未成功或未执行更改。"
)
def add_message_to_normal_chat_cache(self, message: MessageRecv, interest_value: float, is_mentioned: bool):
self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned)
# 如果字典长度超过10删除最旧的消息
if len(self.interest_dict) > 30:
oldest_key = next(iter(self.interest_dict))
self.interest_dict.pop(oldest_key)
def is_in_focus_cooldown(self) -> bool:
"""检查是否在focus模式的冷却期内

View File

@@ -13,6 +13,7 @@ from ..utils.utils import truncate_message, calculate_typing_time, count_message
from src.common.logger import get_logger
from rich.traceback import install
import traceback
install(extra_lines=3)
@@ -292,6 +293,7 @@ class MessageManager:
await asyncio.gather(*tasks)
except Exception as e:
logger.error(f"消息处理循环 gather 出错: {e}")
print(traceback.format_exc())
# 等待一小段时间避免CPU空转
try:

View File

@@ -1,6 +1,6 @@
import asyncio
from typing import Dict, Optional # 重新导入类型
from src.chat.message_receive.message import MessageSending, MessageThinking
from typing import Dict # 重新导入类型
from src.chat.message_receive.message import MessageSending
from src.common.message.api import get_global_api
from src.chat.message_receive.storage import MessageStorage
from src.chat.utils.utils import truncate_message
@@ -36,42 +36,6 @@ class HeartFCSender:
def __init__(self):
self.storage = MessageStorage()
# 用于存储活跃的思考消息
self.thinking_messages: Dict[str, Dict[str, MessageThinking]] = {}
self._thinking_lock = asyncio.Lock() # 保护 thinking_messages 的锁
async def register_thinking(self, thinking_message: MessageThinking):
"""注册一个思考中的消息。"""
if not thinking_message.chat_stream or not thinking_message.message_info.message_id:
logger.error("无法注册缺少 chat_stream 或 message_id 的思考消息")
return
chat_id = thinking_message.chat_stream.stream_id
message_id = thinking_message.message_info.message_id
async with self._thinking_lock:
if chat_id not in self.thinking_messages:
self.thinking_messages[chat_id] = {}
if message_id in self.thinking_messages[chat_id]:
logger.warning(f"[{chat_id}] 尝试注册已存在的思考消息 ID: {message_id}")
self.thinking_messages[chat_id][message_id] = thinking_message
logger.debug(f"[{chat_id}] Registered thinking message: {message_id}")
async def complete_thinking(self, chat_id: str, message_id: str):
"""完成并移除一个思考中的消息记录。"""
async with self._thinking_lock:
if chat_id in self.thinking_messages and message_id in self.thinking_messages[chat_id]:
del self.thinking_messages[chat_id][message_id]
logger.debug(f"[{chat_id}] Completed thinking message: {message_id}")
if not self.thinking_messages[chat_id]:
del self.thinking_messages[chat_id]
logger.debug(f"[{chat_id}] Removed empty thinking message container.")
async def get_thinking_start_time(self, chat_id: str, message_id: str) -> Optional[float]:
"""获取已注册思考消息的开始时间。"""
async with self._thinking_lock:
thinking_message = self.thinking_messages.get(chat_id, {}).get(message_id)
return thinking_message.thinking_start_time if thinking_message else None
async def send_message(self, message: MessageSending, typing=False, set_reply=False, storage_message=True):
"""
@@ -121,5 +85,4 @@ class HeartFCSender:
except Exception as e:
logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}")
raise e
finally:
await self.complete_thinking(chat_id, message_id)

File diff suppressed because it is too large Load Diff

View File

@@ -526,16 +526,24 @@ class ActionModifier:
return removals
def get_available_actions_count(self) -> int:
def get_available_actions_count(self,mode:str = "focus") -> int:
"""获取当前可用动作数量排除默认的no_action"""
current_actions = self.action_manager.get_using_actions_for_mode("normal")
current_actions = self.action_manager.get_using_actions_for_mode(mode)
# 排除no_action如果存在
filtered_actions = {k: v for k, v in current_actions.items() if k != "no_action"}
return len(filtered_actions)
def should_skip_planning(self) -> bool:
def should_skip_planning_for_no_reply(self) -> bool:
"""判断是否应该跳过规划过程"""
available_count = self.get_available_actions_count()
current_actions = self.action_manager.get_using_actions_for_mode("focus")
# 排除no_action如果存在
if len(current_actions) == 1 and "no_reply" in current_actions:
return True
return False
def should_skip_planning_for_no_action(self) -> bool:
"""判断是否应该跳过规划过程"""
available_count = self.action_manager.get_using_actions_for_mode("normal")
if available_count == 0:
logger.debug(f"{self.log_prefix} 没有可用动作,跳过规划")
return True

View File

@@ -64,20 +64,19 @@ def init_prompt():
class ActionPlanner:
def __init__(self, chat_id: str, action_manager: ActionManager, mode: str = "focus"):
def __init__(self, chat_id: str, action_manager: ActionManager):
self.chat_id = chat_id
self.log_prefix = f"[{get_chat_manager().get_stream_name(chat_id) or chat_id}]"
self.mode = mode
self.action_manager = action_manager
# LLM规划器配置
self.planner_llm = LLMRequest(
model=global_config.model.planner,
request_type=f"{self.mode}.planner", # 用于动作规划
request_type="planner", # 用于动作规划
)
self.last_obs_time_mark = 0.0
async def plan(self) -> Dict[str, Any]:
async def plan(self,mode: str = "focus") -> Dict[str, Any]:
"""
规划器 (Planner): 使用LLM根据上下文决定做出什么动作。
"""
@@ -92,7 +91,7 @@ class ActionPlanner:
is_group_chat, chat_target_info = get_chat_type_and_target_info(self.chat_id)
logger.debug(f"{self.log_prefix}获取到聊天信息 - 群聊: {is_group_chat}, 目标信息: {chat_target_info}")
current_available_actions_dict = self.action_manager.get_using_actions_for_mode(self.mode)
current_available_actions_dict = self.action_manager.get_using_actions_for_mode(mode)
# 获取完整的动作信息
all_registered_actions = self.action_manager.get_registered_actions()
@@ -122,6 +121,7 @@ class ActionPlanner:
is_group_chat=is_group_chat, # <-- Pass HFC state
chat_target_info=chat_target_info, # <-- 传递获取到的聊天目标信息
current_available_actions=current_available_actions, # <-- Pass determined actions
mode=mode,
)
# --- 调用 LLM (普通文本生成) ---
@@ -215,6 +215,7 @@ class ActionPlanner:
is_group_chat: bool, # Now passed as argument
chat_target_info: Optional[dict], # Now passed as argument
current_available_actions,
mode: str = "focus",
) -> str:
"""构建 Planner LLM 的提示词 (获取模板并填充数据)"""
try:
@@ -244,7 +245,7 @@ class ActionPlanner:
self.last_obs_time_mark = time.time()
if self.mode == "focus":
if mode == "focus":
by_what = "聊天内容"
no_action_block = ""
else:

View File

@@ -136,33 +136,6 @@ class DefaultReplyer:
selected_config = random.choices(population=configs, weights=weights, k=1)[0]
return selected_config
async def _create_thinking_message(self, anchor_message: Optional[MessageRecv], thinking_id: str):
"""创建思考消息 (尝试锚定到 anchor_message)"""
if not anchor_message or not anchor_message.chat_stream:
logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。")
return None
chat = anchor_message.chat_stream
messageinfo = anchor_message.message_info
thinking_time_point = parse_thinking_id_to_timestamp(thinking_id)
bot_user_info = UserInfo(
user_id=global_config.bot.qq_account,
user_nickname=global_config.bot.nickname,
platform=messageinfo.platform,
)
thinking_message = MessageThinking(
message_id=thinking_id,
chat_stream=chat,
bot_user_info=bot_user_info,
reply=anchor_message, # 回复的是锚点消息
thinking_start_time=thinking_time_point,
)
# logger.debug(f"创建思考消息thinking_message{thinking_message}")
await self.heart_fc_sender.register_thinking(thinking_message)
return None
async def generate_reply_with_context(
self,
reply_data: Dict[str, Any] = None,
@@ -812,108 +785,6 @@ class DefaultReplyer:
return prompt
async def send_response_messages(
self,
anchor_message: Optional[MessageRecv],
response_set: List[Tuple[str, str]],
thinking_id: str = "",
display_message: str = "",
) -> Optional[MessageSending]:
"""发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender"""
chat = self.chat_stream
chat_id = self.chat_stream.stream_id
if chat is None:
logger.error(f"{self.log_prefix} 无法发送回复chat_stream 为空。")
return None
if not anchor_message:
logger.error(f"{self.log_prefix} 无法发送回复anchor_message 为空。")
return None
stream_name = get_chat_manager().get_stream_name(chat_id) or chat_id # 获取流名称用于日志
# 检查思考过程是否仍在进行,并获取开始时间
if thinking_id:
# print(f"thinking_id: {thinking_id}")
thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(chat_id, thinking_id)
else:
print("thinking_id is None")
# thinking_id = "ds" + str(round(time.time(), 2))
thinking_start_time = time.time()
if thinking_start_time is None:
logger.error(f"[{stream_name}]replyer思考过程未找到或已结束无法发送回复。")
return None
mark_head = False
# first_bot_msg: Optional[MessageSending] = None
reply_message_ids = [] # 记录实际发送的消息ID
sent_msg_list = []
for i, msg_text in enumerate(response_set):
# 为每个消息片段生成唯一ID
type = msg_text[0]
data = msg_text[1]
if global_config.debug.debug_show_chat_mode and type == "text":
data += ""
part_message_id = f"{thinking_id}_{i}"
message_segment = Seg(type=type, data=data)
if type == "emoji":
is_emoji = True
else:
is_emoji = False
reply_to = not mark_head
bot_message: MessageSending = await self._build_single_sending_message(
anchor_message=anchor_message,
message_id=part_message_id,
message_segment=message_segment,
display_message=display_message,
reply_to=reply_to,
is_emoji=is_emoji,
thinking_id=thinking_id,
thinking_start_time=thinking_start_time,
)
try:
if (
bot_message.is_private_message()
or bot_message.reply.processed_plain_text != "[System Trigger Context]"
or mark_head
):
set_reply = False
else:
set_reply = True
if not mark_head:
mark_head = True
typing = False
else:
typing = True
sent_msg = await self.heart_fc_sender.send_message(bot_message, typing=typing, set_reply=set_reply)
reply_message_ids.append(part_message_id) # 记录我们生成的ID
sent_msg_list.append((type, sent_msg))
except Exception as e:
logger.error(f"{self.log_prefix}发送回复片段 {i} ({part_message_id}) 时失败: {e}")
traceback.print_exc()
# 这里可以选择是继续发送下一个片段还是中止
# 在尝试发送完所有片段后,完成原始的 thinking_id 状态
try:
await self.heart_fc_sender.complete_thinking(chat_id, thinking_id)
except Exception as e:
logger.error(f"{self.log_prefix}完成思考状态 {thinking_id} 时出错: {e}")
return sent_msg_list
async def _build_single_sending_message(
self,
message_id: str,

View File

@@ -7,7 +7,7 @@ from src.common.remote import TelemetryHeartBeatTask
from src.manager.async_task_manager import async_task_manager
from src.chat.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask
from src.chat.emoji_system.emoji_manager import get_emoji_manager
from src.chat.normal_chat.willing.willing_manager import get_willing_manager
from src.chat.willing.willing_manager import get_willing_manager
from src.chat.message_receive.chat_stream import get_chat_manager
from src.chat.message_receive.normal_message_sender import message_manager
from src.chat.message_receive.storage import MessageStorage

View File

@@ -11,8 +11,6 @@ from src.common.logger import get_logger
# 导入API模块 - 标准Python包方式
from src.plugin_system.apis import message_api
from src.config.config import global_config
from src.chat.memory_system.Hippocampus import hippocampus_manager
import math
logger = get_logger("core_actions")