update:0.6.3 fix4

pull/978/head
SengokuCola 2025-05-18 20:30:00 +08:00
parent cad89735b2
commit d4ee8d3618
27 changed files with 172 additions and 1878 deletions

View File

@ -115,7 +115,7 @@
| 模块 | 主要功能 | 特点 |
|----------|------------------------------------------------------------------|-------|
| 💬 聊天系统 | • **统一调控不同回复逻辑**<br>• 智能交互模式 (普通聊天/专注聊天)<br>• 关键词主动发言<br>• 多模型支持<br>• 动态prompt构建<br>• 私聊功能(PFC)增强 | 拟人化交互 |
| 💬 聊天系统 | • **统一调控不同回复逻辑**<br>• 智能交互模式 (普通聊天/专注水群)<br>• 关键词主动发言<br>• 多模型支持<br>• 动态prompt构建<br>• 私聊功能(PFC)增强 | 拟人化交互 |
| 🧠 心流系统 | • 实时思考生成<br>**智能状态管理**<br>**概率回复机制**<br>• 自动启停机制<br>• 日程系统联动<br>**上下文感知工具调用** | 智能化决策 |
| 🧠 记忆系统 | • **记忆整合与提取**<br>• 海马体记忆机制<br>• 聊天记录概括 | 持久化记忆 |
| 😊 表情系统 | • **全新表情包系统**<br>**优化选择逻辑**<br>• 情绪匹配发送<br>• GIF支持<br>• 自动收集与审查 | 丰富表达 |

View File

@ -1,5 +1,13 @@
# Changelog
## [0.6.3-fix-4] - 2025-5-18
- 0.6.3 的最后一个修复版
### fix1-fix4修复日志
-
## [0.6.3] - 2025-4-15
### 摘要

View File

@ -1,51 +0,0 @@
# Changelog
## [1.0.3] - 2025-3-31
### Added
- 新增了心流相关配置项:
- `heartflow` 配置项,用于控制心流功能
### Removed
- 移除了 `response` 配置项中的 `model_r1_probability``model_v3_probability` 选项
- 移除了次级推理模型相关配置
## [1.0.1] - 2025-3-30
### Added
- 增加了流式输出控制项 `stream`
- 修复 `LLM_Request` 不会自动为 `payload` 增加流式输出标志的问题
## [1.0.0] - 2025-3-30
### Added
- 修复了错误的版本命名
- 杀掉了所有无关文件
## [0.0.11] - 2025-3-12
### Added
- 新增了 `schedule` 配置项,用于配置日程表生成功能
- 新增了 `response_splitter` 配置项,用于控制回复分割
- 新增了 `experimental` 配置项,用于实验性功能开关
- 新增了 `llm_observation``llm_sub_heartflow` 模型配置
- 新增了 `llm_heartflow` 模型配置
- 在 `personality` 配置项中新增了 `prompt_schedule_gen` 参数
### Changed
- 优化了模型配置的组织结构
- 调整了部分配置项的默认值
- 调整了配置项的顺序,将 `groups` 配置项移到了更靠前的位置
- 在 `message` 配置项中:
- 新增了 `model_max_output_length` 参数
- 在 `willing` 配置项中新增了 `emoji_response_penalty` 参数
- 将 `personality` 配置项中的 `prompt_schedule` 重命名为 `prompt_schedule_gen`
### Removed
- 移除了 `min_text_length` 配置项
- 移除了 `cq_code` 配置项
- 移除了 `others` 配置项(其功能已整合到 `experimental` 中)
## [0.0.5] - 2025-3-11
### Added
- 新增了 `alias_names` 配置项,用于指定麦麦的别名。
## [0.0.4] - 2025-3-9
### Added
- 新增了 `memory_ban_words` 配置项,用于指定不希望记忆的词汇。

View File

@ -33,13 +33,7 @@ class APIBotConfig:
gender: str # 性别
appearance: str # 外貌特征描述
# schedule
ENABLE_SCHEDULE_GEN: bool # 是否启用日程生成
ENABLE_SCHEDULE_INTERACTION: bool # 是否启用日程交互
PROMPT_SCHEDULE_GEN: str # 日程生成提示词
SCHEDULE_DOING_UPDATE_INTERVAL: int # 日程进行中更新间隔
SCHEDULE_TEMPERATURE: float # 日程生成温度
TIME_ZONE: str # 时区
# platforms
platforms: Dict[str, str] # 平台信息
@ -47,7 +41,7 @@ class APIBotConfig:
# chat
allow_focus_mode: bool # 是否允许专注模式
base_normal_chat_num: int # 基础普通聊天次数
base_focused_chat_num: int # 基础专注聊天次数
base_focused_chat_num: int # 基础专注水群次数
observation_context_size: int # 观察上下文大小
message_buffer: bool # 是否启用消息缓冲
ban_words: List[str] # 禁止词列表

View File

@ -277,6 +277,22 @@ CHAT_STYLE_CONFIG = {
},
}
NORMAL_CHAT_STYLE_CONFIG = {
"advanced": {
"console_format": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-yellow>普通水群</light-yellow> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 普通水群 | {message}",
},
"simple": {
"console_format": "<level>{time:MM-DD HH:mm}</level> | <fg #55DDFF>普通水群 | {message}</fg #55DDFF>",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 普通水群 | {message}",
},
}
REMOTE_STYLE_CONFIG = {
"advanced": {
"console_format": (
@ -347,14 +363,14 @@ SUBHEARTFLOW_MANAGER_STYLE_CONFIG = {
"console_format": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-blue>麦麦水群[管理]</light-blue> | "
"<light-blue>聊天管理</light-blue> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 聊天管理 | {message}",
},
"simple": {
"console_format": "<level>{time:MM-DD HH:mm}</level> | <fg #005BA2>麦麦水群[管理] | {message}</fg #005BA2>", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}",
"console_format": "<level>{time:MM-DD HH:mm}</level> | <fg #005BA2>聊天管理 | {message}</fg #005BA2>", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 聊天管理 | {message}",
},
}
@ -595,14 +611,14 @@ HFC_STYLE_CONFIG = {
"console_format": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-green>专注聊天</light-green> | "
"<light-green>专注水群</light-green> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注水群 | {message}",
},
"simple": {
"console_format": "<level>{time:MM-DD HH:mm}</level> | <light-green>专注聊天 | {message}</light-green>",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
"console_format": "<level>{time:MM-DD HH:mm}</level> | <light-green>专注水群 | {message}</light-green>",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注水群 | {message}",
},
}
@ -866,6 +882,7 @@ API_SERVER_STYLE_CONFIG = API_SERVER_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT els
INTEREST_CHAT_STYLE_CONFIG = (
INTEREST_CHAT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else INTEREST_CHAT_STYLE_CONFIG["advanced"]
)
NORMAL_CHAT_STYLE_CONFIG = NORMAL_CHAT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else NORMAL_CHAT_STYLE_CONFIG["advanced"]
def is_registered_module(record: dict) -> bool:

View File

@ -25,6 +25,7 @@ from src.common.logger import (
LPMM_STYLE_CONFIG,
HFC_STYLE_CONFIG,
TIANYI_STYLE_CONFIG,
NORMAL_CHAT_STYLE_CONFIG,
REMOTE_STYLE_CONFIG,
TOPIC_STYLE_CONFIG,
SENDER_STYLE_CONFIG,
@ -62,7 +63,7 @@ MODULE_LOGGER_CONFIGS = {
"emoji": EMOJI_STYLE_CONFIG, # 表情包
"sub_heartflow": SUB_HEARTFLOW_STYLE_CONFIG, # 麦麦水群
"sub_heartflow_mind": SUB_HEARTFLOW_MIND_STYLE_CONFIG, # 麦麦小脑袋
"subheartflow_manager": SUBHEARTFLOW_MANAGER_STYLE_CONFIG, # 麦麦水群[管理]
"subheartflow_manager": SUBHEARTFLOW_MANAGER_STYLE_CONFIG, # 聊天管理
"base_tool": BASE_TOOL_STYLE_CONFIG, # 工具使用
"chat_stream": CHAT_STREAM_STYLE_CONFIG, # 聊天流
"person_info": PERSON_INFO_STYLE_CONFIG, # 人物信息
@ -73,6 +74,7 @@ MODULE_LOGGER_CONFIGS = {
"hfc": HFC_STYLE_CONFIG, # HFC
"tianyi": TIANYI_STYLE_CONFIG, # 天依
"remote": REMOTE_STYLE_CONFIG, # 远程
"normal_chat": NORMAL_CHAT_STYLE_CONFIG, # 普通水群
"topic": TOPIC_STYLE_CONFIG, # 话题
"sender": SENDER_STYLE_CONFIG, # 消息发送
"confirm": CONFIRM_STYLE_CONFIG, # EULA与PRIVACY确认

View File

@ -25,7 +25,7 @@ logger = get_logger("config")
# 考虑到实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
is_test = False
mai_version_main = "0.6.3"
mai_version_fix = "fix-3"
mai_version_fix = "fix-4"
if mai_version_fix:
if is_test:
@ -166,18 +166,13 @@ class BotConfig:
gender: str = "" # 性别
appearance: str = "用几句话描述外貌特征" # 外貌特征
# schedule
ENABLE_SCHEDULE_GEN: bool = False # 是否启用日程生成
PROMPT_SCHEDULE_GEN = "无日程"
SCHEDULE_DOING_UPDATE_INTERVAL: int = 300 # 日程表更新间隔 单位秒
SCHEDULE_TEMPERATURE: float = 0.5 # 日程表温度建议0.5-1.0
TIME_ZONE: str = "Asia/Shanghai" # 时区
# chat
allow_focus_mode: bool = True # 是否允许专注聊天状态
allow_focus_mode: bool = True # 是否允许专注水群状态
base_normal_chat_num: int = 3 # 最多允许多少个群进行普通聊天
base_focused_chat_num: int = 2 # 最多允许多少个群进行专注聊天
base_focused_chat_num: int = 2 # 最多允许多少个群进行专注水群
observation_context_size: int = 12 # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩
@ -371,23 +366,7 @@ class BotConfig:
config.gender = identity_config.get("gender", config.gender)
config.appearance = identity_config.get("appearance", config.appearance)
def schedule(parent: dict):
schedule_config = parent["schedule"]
config.ENABLE_SCHEDULE_GEN = schedule_config.get("enable_schedule_gen", config.ENABLE_SCHEDULE_GEN)
config.PROMPT_SCHEDULE_GEN = schedule_config.get("prompt_schedule_gen", config.PROMPT_SCHEDULE_GEN)
config.SCHEDULE_DOING_UPDATE_INTERVAL = schedule_config.get(
"schedule_doing_update_interval", config.SCHEDULE_DOING_UPDATE_INTERVAL
)
logger.info(
f"载入自定义日程prompt:{schedule_config.get('prompt_schedule_gen', config.PROMPT_SCHEDULE_GEN)}"
)
if config.INNER_VERSION in SpecifierSet(">=1.0.2"):
config.SCHEDULE_TEMPERATURE = schedule_config.get("schedule_temperature", config.SCHEDULE_TEMPERATURE)
time_zone = schedule_config.get("time_zone", config.TIME_ZONE)
if tz.gettz(time_zone) is None:
logger.error(f"无效的时区: {time_zone},使用默认值: {config.TIME_ZONE}")
else:
config.TIME_ZONE = time_zone
def emoji(parent: dict):
emoji_config = parent["emoji"]
@ -678,7 +657,6 @@ class BotConfig:
"groups": {"func": groups, "support": ">=0.0.0"},
"personality": {"func": personality, "support": ">=0.0.0"},
"identity": {"func": identity, "support": ">=1.2.4"},
"schedule": {"func": schedule, "support": ">=0.0.11", "necessary": False},
"emoji": {"func": emoji, "support": ">=0.0.0"},
"model": {"func": model, "support": ">=0.0.0"},
"memory": {"func": memory, "support": ">=0.0.0", "necessary": False},

View File

@ -1,60 +0,0 @@
from src.do_tool.tool_can_use.base_tool import BaseTool
from src.plugins.schedule.schedule_generator import bot_schedule
from src.common.logger import get_module_logger
from typing import Any
from datetime import datetime
logger = get_module_logger("get_current_task_tool")
class GetCurrentTaskTool(BaseTool):
"""获取当前正在做的事情/最近的任务工具"""
name = "get_schedule"
description = "获取当前正在做的事情,或者某个时间点/时间段的日程信息"
parameters = {
"type": "object",
"properties": {
"start_time": {"type": "string", "description": "开始时间,格式为'HH:MM'填写current则获取当前任务"},
"end_time": {"type": "string", "description": "结束时间,格式为'HH:MM'填写current则获取当前任务"},
},
"required": ["start_time", "end_time"],
}
async def execute(self, function_args: dict[str, Any], message_txt: str = "") -> dict[str, Any]:
"""执行获取当前任务或指定时间段的日程信息
Args:
function_args: 工具参数
message_txt: 原始消息文本此工具不使用
Returns:
dict: 工具执行结果
"""
start_time = function_args.get("start_time")
end_time = function_args.get("end_time")
# 如果 start_time 或 end_time 为 "current",则获取当前任务
if start_time == "current" or end_time == "current":
current_task = bot_schedule.get_current_num_task(num=1, time_info=True)
current_time = datetime.now().strftime("%H:%M:%S")
current_date = datetime.now().strftime("%Y-%m-%d")
if current_task:
task_info = f"{current_date} {current_time},你在{current_task}"
else:
task_info = f"{current_time} {current_date},没在做任何事情"
# 如果提供了时间范围,则获取该时间段的日程信息
elif start_time and end_time:
tasks = await bot_schedule.get_task_from_time_to_time(start_time, end_time)
if tasks:
task_list = []
for task in tasks:
task_time = task[0].strftime("%H:%M")
task_content = task[1]
task_list.append(f"{task_time}时,{task_content}")
task_info = "\n".join(task_list)
else:
task_info = f"{start_time}{end_time} 之间没有找到日程信息"
else:
task_info = "请提供有效的开始时间和结束时间"
return {"name": "get_current_task", "content": f"日程信息: {task_info}"}

View File

@ -1,241 +0,0 @@
# 心流系统 (Heart Flow System)
## 一条消息是怎么到最终回复的?简明易懂的介绍
1 接受消息由HeartHC_processor处理消息存储消息
1.1 process_message()函数,接受消息
1.2 创建消息对应的聊天流(chat_stream)和子心流(sub_heartflow)
1.3 进行常规消息处理
1.4 存储消息 store_message()
1.5 计算兴趣度Interest
1.6 将消息连同兴趣度存储到内存中的interest_dict(SubHeartflow的属性)
2 根据 sub_heartflow 的聊天状态,决定后续处理流程
2a ABSENT状态不做任何处理
2b CHAT状态送入NormalChat 实例
2c FOCUS状态送入HeartFChatting 实例
b NormalChat工作方式
b.1 启动后台任务 _reply_interested_message持续运行。
b.2 该任务轮询 InterestChatting 提供的 interest_dict
b.3 对每条消息,结合兴趣度、是否被提及(@)、意愿管理器(WillingManager)计算回复概率。这部分要改目前还是用willing计算的之后要和Interest合并
b.4 若概率通过:
b.4.1 创建"思考中"消息 (MessageThinking)。
b.4.2 调用 NormalChatGenerator 生成文本回复。
b.4.3 通过 message_manager 发送回复 (MessageSending)。
b.4.4 可能根据配置和文本内容,额外发送一个匹配的表情包。
b.4.5 更新关系值和全局情绪。
b.5 处理完成后,从 interest_dict 中移除该消息。
c HeartFChatting工作方式
c.1 启动主循环 _hfc_loop
c.2 每个循环称为一个周期 (Cycle),执行 think_plan_execute 流程。
c.3 Think (思考) 阶段:
c.3.1 观察 (Observe): 通过 ChattingObservation使用 observe() 获取最新的聊天消息。
c.3.2 思考 (Think): 调用 SubMind 的 do_thinking_before_reply 方法。
c.3.2.1 SubMind 结合观察到的内容、个性、情绪、上周期动作等信息,生成当前的内心想法 (current_mind)。
c.3.2.2 在此过程中 SubMind 的LLM可能请求调用工具 (ToolUser) 来获取额外信息或执行操作,结果存储在 structured_info 中。
c.4 Plan (规划/决策) 阶段:
c.4.1 结合观察到的消息文本、`SubMind` 生成的 `current_mind``structured_info`、以及 `ActionManager` 提供的可用动作,决定本次周期的行动 (`text_reply`/`emoji_reply`/`no_reply`) 和理由。
c.4.2 重新规划检查 (Re-plan Check): 如果在 c.3.1 到 c.4.1 期间检测到新消息,可能(有概率)触发重新执行 c.4.1 决策步骤。
c.5 Execute (执行/回复) 阶段:
c.5.1 如果决策是 text_reply:
c.5.1.1 获取锚点消息。
c.5.1.2 通过 HeartFCSender 注册"思考中"状态。
c.5.1.3 调用 HeartFCGenerator (gpt_instance) 生成回复文本。
c.5.1.4 通过 HeartFCSender 发送回复
c.5.1.5 如果规划时指定了表情查询 (emoji_query),随后发送表情。
c.5.2 如果决策是 emoji_reply:
c.5.2.1 获取锚点消息。
c.5.2.2 通过 HeartFCSender 直接发送匹配查询 (emoji_query) 的表情。
c.5.3 如果决策是 no_reply:
c.5.3.1 进入等待状态,直到检测到新消息或超时。
c.5.3.2 同时,增加内部连续不回复计数器。如果该计数器达到预设阈值(例如 5 次),则调用初始化时由 `SubHeartflowManager` 提供的回调函数。此回调函数会通知 `SubHeartflowManager` 请求将对应的 `SubHeartflow` 状态转换为 `ABSENT`。如果执行了其他动作(如 `text_reply``emoji_reply`),则此计数器会被重置。
c.6 循环结束后,记录周期信息 (CycleInfo)并根据情况进行短暂休眠防止CPU空转。
## 1. 一条消息是怎么到最终回复的?复杂细致的介绍
### 1.1. 主心流 (Heartflow)
- **文件**: `heartflow.py`
- **职责**:
- 作为整个系统的主控制器。
- 持有并管理 `SubHeartflowManager`,用于管理所有子心流。
- 持有并管理自身状态 `self.current_state: MaiStateInfo`,该状态控制系统的整体行为模式。
- 统筹管理系统后台任务(如消息存储、资源分配等)。
- **注意**: 主心流自身不进行周期性的全局思考更新。
### 1.2. 子心流 (SubHeartflow)
- **文件**: `sub_heartflow.py`
- **职责**:
- 处理具体的交互场景,例如:群聊、私聊、与虚拟主播(vtb)互动、桌面宠物交互等。
- 维护特定场景下的思维状态和聊天流状态 (`ChatState`)。
- 通过关联的 `Observation` 实例接收和处理信息。
- 拥有独立的思考 (`SubMind`) 和回复判断能力。
- **观察者**: 每个子心流可以拥有一个或多个 `Observation` 实例(目前每个子心流仅使用一个 `ChattingObservation`)。
- **内部结构**:
- **聊天流状态 (`ChatState`)**: 标记当前子心流的参与模式 (`ABSENT`, `CHAT`, `FOCUSED`),决定是否观察、回复以及使用何种回复模式。
- **聊天实例 (`NormalChatInstance` / `HeartFlowChatInstance`)**: 根据 `ChatState` 激活对应的实例来处理聊天逻辑。同一时间只有一个实例处于活动状态。
### 1.3. 观察系统 (Observation)
- **文件**: `observation.py`
- **职责**:
- 定义信息输入的来源和格式。
- 为子心流提供其所处环境的信息。
- **当前实现**:
- 目前仅有 `ChattingObservation` 一种观察类型。
- `ChattingObservation` 负责从数据库拉取指定聊天的最新消息,并将其格式化为可读内容,供 `SubHeartflow` 使用。
### 1.4. 子心流管理器 (SubHeartflowManager)
- **文件**: `subheartflow_manager.py`
- **职责**:
- 作为 `Heartflow` 的成员变量存在。
- **在初始化时接收并持有 `Heartflow``MaiStateInfo` 实例。**
- 负责所有 `SubHeartflow` 实例的生命周期管理,包括:
- 创建和获取 (`get_or_create_subheartflow`)。
- 停止和清理 (`sleep_subheartflow`, `cleanup_inactive_subheartflows`)。
- 根据 `Heartflow` 的状态 (`self.mai_state_info`) 和限制条件,激活、停用或调整子心流的状态(例如 `enforce_subheartflow_limits`, `randomly_deactivate_subflows`, `sbhf_absent_into_focus`)。
- **新增**: 通过调用 `sbhf_absent_into_chat` 方法,使用 LLM (配置与 `Heartflow` 主 LLM 相同) 评估处于 `ABSENT``CHAT` 状态的子心流,根据观察到的活动摘要和 `Heartflow` 的当前状态,判断是否应在 `ABSENT``CHAT` 之间进行转换 (同样受限于 `CHAT` 状态的数量上限)。
- **清理机制**: 通过后台任务 (`BackgroundTaskManager`) 定期调用 `cleanup_inactive_subheartflows` 方法,此方法会识别并**删除**那些处于 `ABSENT` 状态超过一小时 (`INACTIVE_THRESHOLD_SECONDS`) 的子心流实例。
### 1.5. 消息处理与回复流程 (Message Processing vs. Replying Flow)
- **关注点分离**: 系统严格区分了接收和处理传入消息的流程与决定和生成回复的流程。
- **消息处理 (Processing)**:
- 由一个独立的处理器(例如 `HeartFCProcessor`)负责接收原始消息数据。
- 职责包括:消息解析 (`MessageRecv`)、过滤(屏蔽词、正则表达式)、基于记忆系统的初步兴趣计算 (`HippocampusManager`)、消息存储 (`MessageStorage`) 以及用户关系更新 (`RelationshipManager`)。
- 处理后的消息信息(如计算出的兴趣度)会传递给对应的 `SubHeartflow`
- **回复决策与生成 (Replying)**:
- 由 `SubHeartflow` 及其当前激活的聊天实例 (`NormalChatInstance` 或 `HeartFlowChatInstance`) 负责。
- 基于其内部状态 (`ChatState`、`SubMind` 的思考结果)、观察到的信息 (`Observation` 提供的内容) 以及 `InterestChatting` 的状态来决定是否回复、何时回复以及如何回复。
- **消息缓冲 (Message Caching)**:
- `message_buffer` 模块会对某些传入消息进行临时缓存,尤其是在处理连续的多部分消息(如多张图片)时。
- 这个缓冲机制发生在 `HeartFCProcessor` 处理流程中,确保消息的完整性,然后才进行后续的存储和兴趣计算。
- 缓存的消息最终仍会流向对应的 `ChatStream`(与 `SubHeartflow` 关联),但核心的消息处理与回复决策仍然是分离的步骤。
## 2. 核心控制与状态管理 (Core Control and State Management)
### 2.1. Heart Flow 整体控制
- **控制者**: 主心流 (`Heartflow`)
- **核心职责**:
- 通过其成员 `SubHeartflowManager` 创建和管理子心流(**在创建 `SubHeartflowManager` 时会传入自身的 `MaiStateInfo`**)。
- 通过其成员 `self.current_state: MaiStateInfo` 控制整体行为模式。
- 管理系统级后台任务。
- **注意**: 不再提供直接获取所有子心流 ID (`get_all_subheartflows_streams_ids`) 的公共方法。
### 2.2. Heart Flow 状态 (`MaiStateInfo`)
- **定义与管理**: `Heartflow` 持有 `MaiStateInfo` 的实例 (`self.current_state`) 来管理其状态。状态的枚举定义在 `my_state_manager.py` 中的 `MaiState`
- **状态及含义**:
- `MaiState.OFFLINE` (不在线): 不观察任何群消息,不进行主动交互,仅存储消息。当主状态变为 `OFFLINE` 时,`SubHeartflowManager` 会将所有子心流的状态设置为 `ChatState.ABSENT`
- `MaiState.PEEKING` (看一眼手机): 有限度地参与聊天(由 `MaiStateInfo` 定义具体的普通/专注群数量限制)。
- `MaiState.NORMAL_CHAT` (正常看手机): 正常参与聊天,允许 `SubHeartflow` 进入 `CHAT``FOCUSED` 状态(数量受限)。
* `MaiState.FOCUSED_CHAT` (专心看手机): 更积极地参与聊天,通常允许更多或更高优先级的 `FOCUSED` 状态子心流。
- **当前转换逻辑**: 目前,`MaiState` 之间的转换由 `MaiStateManager` 管理,主要基于状态持续时间和随机概率。这是一种临时的实现方式,未来计划进行改进。
- **作用**: `Heartflow` 的状态直接影响 `SubHeartflowManager` 如何管理子心流(如激活数量、允许的状态等)。
### 2.3. 聊天流状态 (`ChatState`) 与转换
- **管理对象**: 每个 `SubHeartflow` 实例内部维护其 `ChatStateInfo`,包含当前的 `ChatState`
- **状态及含义**:
- `ChatState.ABSENT` (不参与/没在看): 初始或停用状态。子心流不观察新信息,不进行思考,也不回复。
- `ChatState.CHAT` (随便看看/水群): 普通聊天模式。激活 `NormalChatInstance`
* `ChatState.FOCUSED` (专注/认真水群): 专注聊天模式。激活 `HeartFlowChatInstance`
- **选择**: 子心流可以根据外部指令(来自 `SubHeartflowManager`)或内部逻辑(未来的扩展)选择进入 `ABSENT` 状态(不回复不观察),或进入 `CHAT` / `FOCUSED` 中的一种回复模式。
- **状态转换机制** (由 `SubHeartflowManager` 驱动,更细致的说明):
- **初始状态**: 新创建的 `SubHeartflow` 默认为 `ABSENT` 状态。
- **`ABSENT` -> `CHAT` (激活闲聊)**:
- **触发条件**: `Heartflow` 的主状态 (`MaiState`) 允许 `CHAT` 模式,且当前 `CHAT` 状态的子心流数量未达上限。
- **判定机制**: `SubHeartflowManager` 中的 `sbhf_absent_into_chat` 方法调用大模型(LLM)。LLM 读取该群聊的近期内容和结合自身个性信息,判断是否"想"在该群开始聊天。
- **执行**: 若 LLM 判断为是,且名额未满,`SubHeartflowManager` 调用 `change_chat_state(ChatState.CHAT)`
- **`CHAT` -> `FOCUSED` (激活专注)**:
- **触发条件**: 子心流处于 `CHAT` 状态,其内部维护的"开屎热聊"概率 (`InterestChatting.start_hfc_probability`) 达到预设阈值(表示对当前聊天兴趣浓厚),同时 `Heartflow` 的主状态允许 `FOCUSED` 模式,且 `FOCUSED` 名额未满。
- **判定机制**: `SubHeartflowManager` 中的 `sbhf_absent_into_focus` 方法定期检查满足条件的 `CHAT` 子心流。
- **执行**: 若满足所有条件,`SubHeartflowManager` 调用 `change_chat_state(ChatState.FOCUSED)`
- **注意**: 无法从 `ABSENT` 直接跳到 `FOCUSED`,必须先经过 `CHAT`
- **`FOCUSED` -> `ABSENT` (退出专注)**:
- **主要途径 (内部驱动)**: 在 `FOCUSED` 状态下运行的 `HeartFlowChatInstance` 连续多次决策为 `no_reply` (例如达到 5 次,次数可配),它会通过回调函数 (`sbhf_focus_into_absent`) 请求 `SubHeartflowManager` 将其状态**直接**设置为 `ABSENT`
- **其他途径 (外部驱动)**:
- `Heartflow` 主状态变为 `OFFLINE``SubHeartflowManager` 强制所有子心流变为 `ABSENT`
- `SubHeartflowManager``FOCUSED` 名额超限 (`enforce_subheartflow_limits`) 或随机停用 (`randomly_deactivate_subflows`) 而将其设置为 `ABSENT`
- **`CHAT` -> `ABSENT` (退出闲聊)**:
- **主要途径 (内部驱动)**: `SubHeartflowManager` 中的 `sbhf_absent_into_chat` 方法调用 LLM。LLM 读取群聊内容和结合自身状态,判断是否"不想"继续在此群闲聊。
- **执行**: 若 LLM 判断为是,`SubHeartflowManager` 调用 `change_chat_state(ChatState.ABSENT)`
- **其他途径 (外部驱动)**:
- `Heartflow` 主状态变为 `OFFLINE`
- `SubHeartflowManager``CHAT` 名额超限或随机停用。
- **全局强制 `ABSENT`**: 当 `Heartflow``MaiState` 变为 `OFFLINE` 时,`SubHeartflowManager` 会调用所有子心流的 `change_chat_state(ChatState.ABSENT)`,强制它们全部停止活动。
- **状态变更执行者**: `change_chat_state` 方法仅负责执行状态的切换和对应聊天实例的启停,不进行名额检查。名额检查的责任由 `SubHeartflowManager` 中的各个决策方法承担。
- **最终清理**: 进入 `ABSENT` 状态的子心流不会立即被删除,只有在 `ABSENT` 状态持续一小时 (`INACTIVE_THRESHOLD_SECONDS`) 后,才会被后台清理任务 (`cleanup_inactive_subheartflows`) 删除。
## 3. 聊天实例详解 (Chat Instances Explained)
### 3.1. NormalChatInstance
- **激活条件**: 对应 `SubHeartflow``ChatState``CHAT`
- **工作流程**:
- 当 `SubHeartflow` 进入 `CHAT` 状态时,`NormalChatInstance` 会被激活。
- 实例启动后,会创建一个后台任务 (`_reply_interested_message`)。
- 该任务持续监控由 `InterestChatting` 传入的、具有一定兴趣度的消息列表 (`interest_dict`)。
- 对列表中的每条消息,结合是否被提及 (`@`)、消息本身的兴趣度以及当前的回复意愿 (`WillingManager`),计算出一个回复概率。
- 根据计算出的概率随机决定是否对该消息进行回复。
- 如果决定回复,则调用 `NormalChatGenerator` 生成回复内容,并可能附带表情包。
- **行为特点**:
- 回复相对常规、简单。
- 不投入过多计算资源。
- 侧重于维持基本的交流氛围。
- 示例:对问候语、日常分享等进行简单回应。
### 3.2. HeartFlowChatInstance (继承自原 PFC 逻辑)
- **激活条件**: 对应 `SubHeartflow``ChatState``FOCUSED`
- **工作流程**:
- 基于更复杂的规则(原 PFC 模式)进行深度处理。
- 对群内话题进行深入分析。
- 可能主动发起相关话题或引导交流。
- **行为特点**:
- 回复更积极、深入。
- 投入更多资源参与聊天。
- 回复内容可能更详细、有针对性。
- 对话题参与度高,能带动交流。
- 示例:对复杂或有争议话题阐述观点,并与人互动。
## 4. 工作流程示例 (Example Workflow)
1. **启动**: `Heartflow` 启动,初始化 `MaiStateInfo` (例如 `OFFLINE`) 和 `SubHeartflowManager`
2. **状态变化**: 用户操作或内部逻辑使 `Heartflow``current_state` 变为 `NORMAL_CHAT`
3. **管理器响应**: `SubHeartflowManager` 检测到状态变化,根据 `NORMAL_CHAT` 的限制,调用 `get_or_create_subheartflow` 获取或创建子心流,并通过 `change_chat_state` 将部分子心流状态从 `ABSENT` 激活为 `CHAT`
4. **子心流激活**: 被激活的 `SubHeartflow` 启动其 `NormalChatInstance`
5. **信息接收**: 该 `SubHeartflow``ChattingObservation` 开始从数据库拉取新消息。
6. **普通回复**: `NormalChatInstance` 处理观察到的信息,执行普通回复逻辑。
7. **兴趣评估**: `SubHeartflowManager` 定期评估该子心流的 `InterestChatting` 状态。
8. **提升状态**: 若兴趣度达标且 `Heartflow` 状态允许,`SubHeartflowManager` 调用该子心流的 `change_chat_state` 将其状态提升为 `FOCUSED`
9. **子心流切换**: `SubHeartflow` 内部停止 `NormalChatInstance`,启动 `HeartFlowChatInstance`
10. **专注回复**: `HeartFlowChatInstance` 开始根据其逻辑进行更深入的交互。
11. **状态回落/停用**: 若 `Heartflow` 状态变为 `OFFLINE``SubHeartflowManager` 会调用所有活跃子心流的 `change_chat_state(ChatState.ABSENT)`,使其进入 `ABSENT` 状态(它们不会立即被删除,只有在 `ABSENT` 状态持续1小时后才会被清理
## 5. 使用与配置 (Usage and Configuration)
### 5.1. 使用说明 (Code Examples)
- **(内部)创建/获取子心流** (由 `SubHeartflowManager` 调用, 示例):
```python
# subheartflow_manager.py (get_or_create_subheartflow 内部)
# 注意mai_states 现在是 self.mai_state_info
new_subflow = SubHeartflow(subheartflow_id, self.mai_state_info)
await new_subflow.initialize()
observation = ChattingObservation(chat_id=subheartflow_id)
new_subflow.add_observation(observation)
```
- **(内部)添加观察者** (由 `SubHeartflowManager``SubHeartflow` 内部调用):
```python
# sub_heartflow.py
self.observations.append(observation)
```

View File

@ -13,17 +13,11 @@ from src.heart_flow.interest_logger import InterestLogger
logger = get_logger("background_tasks")
# 新增兴趣评估间隔
# 兴趣评估间隔
INTEREST_EVAL_INTERVAL_SECONDS = 5
# 新增聊天超时检查间隔
NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS = 60
# 新增状态评估间隔
HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS = 20
# 新增私聊激活检查间隔
PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS = 5 # 与兴趣评估类似设为5秒
CLEANUP_INTERVAL_SECONDS = 1200
STATE_UPDATE_INTERVAL_SECONDS = 60
# 专注水群检查间隔
FOCUS_CHAT_CHECK_INTERVAL_SECONDS = 60
# 日志记录间隔
LOG_INTERVAL_SECONDS = 3
@ -70,21 +64,16 @@ class BackgroundTaskManager:
self.interest_logger = interest_logger
# Task references
self._state_update_task: Optional[asyncio.Task] = None
self._cleanup_task: Optional[asyncio.Task] = None
self._logging_task: Optional[asyncio.Task] = None
self._normal_chat_timeout_check_task: Optional[asyncio.Task] = None
self._hf_judge_state_update_task: Optional[asyncio.Task] = None
self._focus_chat_check_task: Optional[asyncio.Task] = None
self._into_focus_task: Optional[asyncio.Task] = None
self._private_chat_activation_task: Optional[asyncio.Task] = None # 新增私聊激活任务引用
self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks
self._detect_command_from_gui_task: Optional[asyncio.Task] = None # 新增GUI命令检测任务引用
async def start_tasks(self):
"""启动所有后台任务
功能说明:
- 启动核心后台任务: 状态更新清理日志记录兴趣评估和随机停用
- 启动核心后台任务: 日志记录兴趣评估和专注水群监测
- 每个任务启动前检查是否已在运行
- 将任务引用保存到任务列表
"""
@ -92,28 +81,10 @@ class BackgroundTaskManager:
# 任务配置列表: (任务函数, 任务名称, 日志级别, 额外日志信息, 任务对象引用属性名)
task_configs = [
(
lambda: self._run_state_update_cycle(STATE_UPDATE_INTERVAL_SECONDS),
lambda: self._run_focus_chat_check_cycle(FOCUS_CHAT_CHECK_INTERVAL_SECONDS),
"debug",
f"聊天状态更新任务已启动 间隔:{STATE_UPDATE_INTERVAL_SECONDS}s",
"_state_update_task",
),
(
lambda: self._run_normal_chat_timeout_check_cycle(NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS),
"debug",
f"聊天超时检查任务已启动 间隔:{NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS}s",
"_normal_chat_timeout_check_task",
),
(
lambda: self._run_absent_into_chat(HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS),
"debug",
f"状态评估任务已启动 间隔:{HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS}s",
"_hf_judge_state_update_task",
),
(
self._run_cleanup_cycle,
"info",
f"清理任务已启动 间隔:{CLEANUP_INTERVAL_SECONDS}s",
"_cleanup_task",
f"专注水群检查任务已启动 间隔:{FOCUS_CHAT_CHECK_INTERVAL_SECONDS}s",
"_focus_chat_check_task",
),
(
self._run_logging_cycle,
@ -121,28 +92,13 @@ class BackgroundTaskManager:
f"日志任务已启动 间隔:{LOG_INTERVAL_SECONDS}s",
"_logging_task",
),
# 新增兴趣评估任务配置
# 兴趣评估任务配置
(
self._run_into_focus_cycle,
"debug", # 设为debug避免过多日志
f"专注评估任务已启动 间隔:{INTEREST_EVAL_INTERVAL_SECONDS}s",
"_into_focus_task",
),
# 新增私聊激活任务配置
(
# Use lambda to pass the interval to the runner function
lambda: self._run_private_chat_activation_cycle(PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS),
"debug",
f"私聊激活检查任务已启动 间隔:{PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS}s",
"_private_chat_activation_task",
),
# 新增GUI命令检测任务配置
# (
# lambda: self._run_detect_command_from_gui_cycle(3),
# "debug",
# f"GUI命令检测任务已启动 间隔:{3}s",
# "_detect_command_from_gui_task",
# ),
]
# 统一启动所有任务
@ -189,99 +145,24 @@ class BackgroundTaskManager:
# 第三步:清空任务列表
self._tasks = [] # 重置任务列表
async def _perform_state_update_work(self):
"""执行状态更新工作"""
previous_status = self.mai_state_info.get_current_state()
next_state = self.mai_state_manager.check_and_decide_next_state(self.mai_state_info)
state_changed = False
if next_state is not None:
state_changed = self.mai_state_info.update_mai_status(next_state)
# 处理保持离线状态的特殊情况
if not state_changed and next_state == previous_status == self.mai_state_info.mai_status.OFFLINE:
self.mai_state_info.reset_state_timer()
logger.debug("[后台任务] 保持离线状态并重置计时器")
state_changed = True # 触发后续处理
if state_changed:
current_state = self.mai_state_info.get_current_state()
await self.subheartflow_manager.enforce_subheartflow_limits()
# 状态转换处理
if (
current_state == self.mai_state_info.mai_status.OFFLINE
and previous_status != self.mai_state_info.mai_status.OFFLINE
):
logger.info("检测到离线,停用所有子心流")
await self.subheartflow_manager.deactivate_all_subflows()
async def _perform_absent_into_chat(self):
"""调用llm检测是否转换ABSENT-CHAT状态"""
logger.debug("[状态评估任务] 开始基于LLM评估子心流状态...")
await self.subheartflow_manager.sbhf_absent_into_chat()
async def _normal_chat_timeout_check_work(self):
"""检查处于CHAT状态的子心流是否因长时间未发言而超时并将其转为ABSENT"""
logger.debug("[聊天超时检查] 开始检查处于CHAT状态的子心流...")
await self.subheartflow_manager.sbhf_chat_into_absent()
async def _perform_cleanup_work(self):
"""执行子心流清理任务
1. 获取需要清理的不活跃子心流列表
2. 逐个停止这些子心流
3. 记录清理结果
"""
# 获取需要清理的子心流列表(包含ID和原因)
flows_to_stop = self.subheartflow_manager.get_inactive_subheartflows()
if not flows_to_stop:
return # 没有需要清理的子心流直接返回
logger.info(f"准备删除 {len(flows_to_stop)} 个不活跃(1h)子心流")
stopped_count = 0
# 逐个停止子心流
for flow_id in flows_to_stop:
success = await self.subheartflow_manager.delete_subflow(flow_id)
if success:
stopped_count += 1
logger.debug(f"[清理任务] 已停止子心流 {flow_id}")
# 记录最终清理结果
logger.info(f"[清理任务] 清理完成, 共停止 {stopped_count}/{len(flows_to_stop)} 个子心流")
async def _focus_chat_check_work(self):
"""检查FOCUSED状态的子心流是否需要转回CHAT状态"""
logger.debug("[专注水群检查] 开始检查专注水群状态的子心流...")
await self.subheartflow_manager.sbhf_focus_into_chat()
async def _perform_logging_work(self):
"""执行一轮状态日志记录。"""
await self.interest_logger.log_all_states()
# --- 新增兴趣评估工作函数 ---
async def _perform_into_focus_work(self):
"""执行一轮子心流兴趣评估与提升检查。"""
# 直接调用 subheartflow_manager 的方法,并传递当前状态信息
await self.subheartflow_manager.sbhf_absent_into_focus()
# --- 结束新增 ---
# --- 结束新增 ---
# 直接调用 subheartflow_manager 的方法进行CHAT到FOCUSED的转换
await self.subheartflow_manager.sbhf_chat_into_focus()
# --- Specific Task Runners --- #
async def _run_state_update_cycle(self, interval: int):
await _run_periodic_loop(task_name="State Update", interval=interval, task_func=self._perform_state_update_work)
async def _run_absent_into_chat(self, interval: int):
await _run_periodic_loop(task_name="Into Chat", interval=interval, task_func=self._perform_absent_into_chat)
async def _run_normal_chat_timeout_check_cycle(self, interval: int):
async def _run_focus_chat_check_cycle(self, interval: int):
await _run_periodic_loop(
task_name="Normal Chat Timeout Check", interval=interval, task_func=self._normal_chat_timeout_check_work
)
async def _run_cleanup_cycle(self):
await _run_periodic_loop(
task_name="Subflow Cleanup", interval=CLEANUP_INTERVAL_SECONDS, task_func=self._perform_cleanup_work
task_name="Focus Chat Check", interval=interval, task_func=self._focus_chat_check_work
)
async def _run_logging_cycle(self):
@ -289,26 +170,9 @@ class BackgroundTaskManager:
task_name="State Logging", interval=LOG_INTERVAL_SECONDS, task_func=self._perform_logging_work
)
# --- 新增兴趣评估任务运行器 ---
async def _run_into_focus_cycle(self):
await _run_periodic_loop(
task_name="Into Focus",
interval=INTEREST_EVAL_INTERVAL_SECONDS,
task_func=self._perform_into_focus_work,
)
# 新增私聊激活任务运行器
async def _run_private_chat_activation_cycle(self, interval: int):
await _run_periodic_loop(
task_name="Private Chat Activation Check",
interval=interval,
task_func=self.subheartflow_manager.sbhf_absent_private_into_focus,
)
# # 有api之后删除
# async def _run_detect_command_from_gui_cycle(self, interval: int):
# await _run_periodic_loop(
# task_name="Detect Command from GUI",
# interval=interval,
# task_func=self.subheartflow_manager.detect_command_from_gui,
# )

View File

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

View File

@ -1,7 +1,6 @@
from src.heart_flow.sub_heartflow import SubHeartflow, ChatState
from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
from src.plugins.schedule.schedule_generator import bot_schedule
from src.common.logger_manager import get_logger
from typing import Any, Optional
from src.do_tool.tool_use import ToolUser
@ -97,16 +96,5 @@ class Heartflow:
await self.subheartflow_manager.deactivate_all_subflows()
logger.info("[Heartflow] 所有任务和子心流已停止")
async def do_a_thinking(self):
"""执行一次主心流思考过程"""
schedule_info = bot_schedule.get_current_num_task(num=4, time_info=True)
new_mind = await self.mind.do_a_thinking(
current_main_mind=self.current_mind, mai_state_info=self.current_state, schedule_info=schedule_info
)
self.past_mind.append(self.current_mind)
self.current_mind = new_mind
logger.info(f"麦麦的总体脑内状态更新为:{self.current_mind[:100]}...")
self.mind.update_subflows_with_main_mind(new_mind)
heartflow = Heartflow()

View File

@ -145,7 +145,7 @@ class InterestChatting:
}
# --- 新增后台更新任务相关方法 ---
async def _run_update_loop(self, update_interval: float = 1.0):
async def _run_update_loop(self, update_interval: float = 0.5):
"""后台循环,定期更新兴趣和回复概率。"""
try:
while not self._stop_event.is_set():

View File

@ -1,125 +1,45 @@
import enum
import time
import random
from typing import List, Tuple, Optional
from src.common.logger_manager import get_logger
from src.plugins.moods.moods import MoodManager
from src.config.config import global_config
logger = get_logger("mai_state")
# -- 状态相关的可配置参数 (可以从 glocal_config 加载) --
# The line `enable_unlimited_hfc_chat = False` is setting a configuration parameter that controls
# whether a specific debugging feature is enabled or not. When `enable_unlimited_hfc_chat` is set to
# `False`, it means that the debugging feature for unlimited focused chatting is disabled.
# enable_unlimited_hfc_chat = True # 调试用:无限专注聊天
enable_unlimited_hfc_chat = False
prevent_offline_state = True
# 目前默认不启用OFFLINE状态
# 不同状态下普通聊天的最大消息数
base_normal_chat_num = global_config.base_normal_chat_num
base_focused_chat_num = global_config.base_focused_chat_num
MAX_NORMAL_CHAT_NUM_PEEKING = int(base_normal_chat_num / 2)
MAX_NORMAL_CHAT_NUM_NORMAL = base_normal_chat_num
MAX_NORMAL_CHAT_NUM_FOCUSED = base_normal_chat_num + 1
# 不同状态下专注聊天的最大消息数
MAX_FOCUSED_CHAT_NUM_PEEKING = int(base_focused_chat_num / 2)
MAX_FOCUSED_CHAT_NUM_NORMAL = base_focused_chat_num
MAX_FOCUSED_CHAT_NUM_FOCUSED = base_focused_chat_num + 2
# -- 状态定义 --
class MaiState(enum.Enum):
"""
聊天状态:
OFFLINE: 不在线回复概率极低不会进行任何聊天
PEEKING: 看一眼手机回复概率较低会进行一些普通聊天
NORMAL_CHAT: 正常看手机回复概率较高会进行一些普通聊天和少量的专注聊天
FOCUSED_CHAT: 专注聊天回复概率极高会进行专注聊天和少量的普通聊天
NORMAL_CHAT: 正常看手机回复概率较高会进行一些普通聊天和少量的专注水群
FOCUSED_CHAT: 专注水群回复概率极高会进行专注水群和少量的普通聊天
"""
OFFLINE = "不在线"
PEEKING = "看一眼手机"
NORMAL_CHAT = "正常看手机"
FOCUSED_CHAT = "专心看手机"
def get_normal_chat_max_num(self):
# 调试用
if enable_unlimited_hfc_chat:
return 1000
if self == MaiState.OFFLINE:
return 0
elif self == MaiState.PEEKING:
return MAX_NORMAL_CHAT_NUM_PEEKING
elif self == MaiState.NORMAL_CHAT:
return MAX_NORMAL_CHAT_NUM_NORMAL
elif self == MaiState.FOCUSED_CHAT:
return MAX_NORMAL_CHAT_NUM_FOCUSED
return None
def get_focused_chat_max_num(self):
# 调试用
if enable_unlimited_hfc_chat:
return 1000
if self == MaiState.OFFLINE:
return 0
elif self == MaiState.PEEKING:
return MAX_FOCUSED_CHAT_NUM_PEEKING
elif self == MaiState.NORMAL_CHAT:
return MAX_FOCUSED_CHAT_NUM_NORMAL
elif self == MaiState.FOCUSED_CHAT:
return MAX_FOCUSED_CHAT_NUM_FOCUSED
return None
class MaiStateInfo:
def __init__(self):
self.mai_status: MaiState = MaiState.OFFLINE
self.mai_status: MaiState = MaiState.NORMAL_CHAT
self.mai_status_history: List[Tuple[MaiState, float]] = [] # 历史状态,包含 状态,时间戳
self.last_status_change_time: float = time.time() # 状态最后改变时间
self.last_min_check_time: float = time.time() # 上次1分钟规则检查时间
# Mood management is now part of MaiStateInfo
self.mood_manager = MoodManager.get_instance() # Use singleton instance
def update_mai_status(self, new_status: MaiState) -> bool:
"""
更新聊天状态
更新聊天状态始终返回False因为状态永远固定为NORMAL_CHAT
Args:
new_status: 新的 MaiState 状态
Returns:
bool: 如果状态实际发生了改变则返回 True否则返回 False
bool: 始终返回False表示状态不会改变
"""
if new_status != self.mai_status:
self.mai_status = new_status
current_time = time.time()
self.last_status_change_time = current_time
self.last_min_check_time = current_time # Reset 1-min check on any state change
self.mai_status_history.append((new_status, current_time))
logger.info(f"麦麦状态更新为: {self.mai_status.value}")
return True
else:
return False
def reset_state_timer(self):
"""
重置状态持续时间计时器和一分钟规则检查计时器
通常在状态保持不变但需要重新开始计时的情况下调用例如保持 OFFLINE
"""
current_time = time.time()
self.last_status_change_time = current_time
self.last_min_check_time = current_time # Also reset the 1-min check timer
logger.debug("MaiStateInfo 状态计时器已重置。")
# 不再允许状态变化始终保持NORMAL_CHAT
logger.debug(f"尝试将状态更新为: {new_status.value},但状态已固定为 {self.mai_status.value}")
return False
def get_mood_prompt(self) -> str:
"""获取当前的心情提示词"""
@ -128,7 +48,7 @@ class MaiStateInfo:
def get_current_state(self) -> MaiState:
"""获取当前的 MaiState"""
return self.mai_status
return MaiState.NORMAL_CHAT # 始终返回NORMAL_CHAT
class MaiStateManager:
@ -140,106 +60,14 @@ class MaiStateManager:
@staticmethod
def check_and_decide_next_state(current_state_info: MaiStateInfo) -> Optional[MaiState]:
"""
根据当前状态和规则检查是否需要转换状态并决定下一个状态
状态检查函数由于状态已固定为NORMAL_CHAT此函数始终返回None
Args:
current_state_info: 当前的 MaiStateInfo 实例
Returns:
Optional[MaiState]: 如果需要转换返回目标 MaiState否则返回 None
Optional[MaiState]: 始终返回None表示没有状态变化
"""
current_time = time.time()
current_status = current_state_info.mai_status
time_in_current_status = current_time - current_state_info.last_status_change_time
time_since_last_min_check = current_time - current_state_info.last_min_check_time
next_state: Optional[MaiState] = None
# 辅助函数:根据 prevent_offline_state 标志调整目标状态
def _resolve_offline(candidate_state: MaiState) -> MaiState:
if prevent_offline_state and candidate_state == MaiState.OFFLINE:
logger.debug("阻止进入 OFFLINE改为 PEEKING")
return MaiState.PEEKING
return candidate_state
if current_status == MaiState.OFFLINE:
logger.info("当前[离线],没看手机,思考要不要上线看看......")
elif current_status == MaiState.PEEKING:
logger.info("当前[看一眼手机],思考要不要继续聊下去......")
elif current_status == MaiState.NORMAL_CHAT:
logger.info("当前在[正常看手机]思考要不要继续聊下去......")
elif current_status == MaiState.FOCUSED_CHAT:
logger.info("当前在[专心看手机]思考要不要继续聊下去......")
# 1. 麦麦每分钟都有概率离线
if time_since_last_min_check >= 60:
if current_status != MaiState.OFFLINE:
if random.random() < 0.03: # 3% 概率切换到 OFFLINE
potential_next = MaiState.OFFLINE
resolved_next = _resolve_offline(potential_next)
logger.debug(f"概率触发下线resolve 为 {resolved_next.value}")
# 只有当解析后的状态与当前状态不同时才设置 next_state
if resolved_next != current_status:
next_state = resolved_next
# 2. 状态持续时间规则 (只有在规则1没有触发状态改变时才检查)
if next_state is None:
time_limit_exceeded = False
choices_list = []
weights = []
rule_id = ""
if current_status == MaiState.OFFLINE:
# 注意:即使 prevent_offline_state=True也可能从初始的 OFFLINE 状态启动
if time_in_current_status >= 60:
time_limit_exceeded = True
rule_id = "2.1 (From OFFLINE)"
weights = [30, 30, 20, 20]
choices_list = [MaiState.PEEKING, MaiState.NORMAL_CHAT, MaiState.FOCUSED_CHAT, MaiState.OFFLINE]
elif current_status == MaiState.PEEKING:
if time_in_current_status >= 600: # PEEKING 最多持续 600 秒
time_limit_exceeded = True
rule_id = "2.2 (From PEEKING)"
weights = [70, 20, 10]
choices_list = [MaiState.OFFLINE, MaiState.NORMAL_CHAT, MaiState.FOCUSED_CHAT]
elif current_status == MaiState.NORMAL_CHAT:
if time_in_current_status >= 300: # NORMAL_CHAT 最多持续 300 秒
time_limit_exceeded = True
rule_id = "2.3 (From NORMAL_CHAT)"
weights = [50, 50]
choices_list = [MaiState.OFFLINE, MaiState.FOCUSED_CHAT]
elif current_status == MaiState.FOCUSED_CHAT:
if time_in_current_status >= 600: # FOCUSED_CHAT 最多持续 600 秒
time_limit_exceeded = True
rule_id = "2.4 (From FOCUSED_CHAT)"
weights = [80, 20]
choices_list = [MaiState.OFFLINE, MaiState.NORMAL_CHAT]
if time_limit_exceeded:
next_state_candidate = random.choices(choices_list, weights=weights, k=1)[0]
resolved_candidate = _resolve_offline(next_state_candidate)
logger.debug(
f"规则{rule_id}:时间到,随机选择 {next_state_candidate.value}resolve 为 {resolved_candidate.value}"
)
next_state = resolved_candidate # 直接使用解析后的状态
# 注意enable_unlimited_hfc_chat 优先级高于 prevent_offline_state
# 如果触发了这个它会覆盖上面规则2设置的 next_state
if enable_unlimited_hfc_chat:
logger.debug("调试用:开挂了,强制切换到专注聊天")
next_state = MaiState.FOCUSED_CHAT
# --- 最终决策 --- #
# 如果决定了下一个状态,且这个状态与当前状态不同,则返回下一个状态
if next_state is not None and next_state != current_status:
return next_state
# 如果决定保持 OFFLINE (next_state == MaiState.OFFLINE) 且当前也是 OFFLINE
# 并且是由于持续时间规则触发的,返回 OFFLINE 以便调用者可以重置计时器。
# 注意:这个分支只有在 prevent_offline_state = False 时才可能被触发。
elif next_state == MaiState.OFFLINE and current_status == MaiState.OFFLINE and time_in_current_status >= 60:
logger.debug("决定保持 OFFLINE (持续时间规则),返回 OFFLINE 以提示重置计时器。")
return MaiState.OFFLINE # Return OFFLINE to signal caller that timer reset might be needed
else:
# 1. next_state is None (没有触发任何转换规则)
# 2. next_state is not None 但等于 current_status (例如规则1想切OFFLINE但被resolve成PEEKING而当前已经是PEEKING)
# 3. next_state is OFFLINE, current is OFFLINE, 但不是因为时间规则触发 (例如初始状态还没到60秒)
return None # 没有状态转换发生或无需重置计时器
# 不再需要检查或决定状态变化状态已固定为NORMAL_CHAT
logger.debug("当前在[正常看手机]状态,状态已固定,不会变化")
return None # 没有状态转换发生

View File

@ -24,14 +24,13 @@ class Mind:
self.llm_model = llm_model
self.individuality = Individuality.get_instance()
async def do_a_thinking(self, current_main_mind: str, mai_state_info: "MaiStateInfo", schedule_info: str):
async def do_a_thinking(self, current_main_mind: str, mai_state_info: "MaiStateInfo"):
"""
执行一次主心流思考过程生成新的内心独白
Args:
current_main_mind: 当前的主心流想法
mai_state_info: 当前的 Mai 状态信息 (用于获取 mood)
schedule_info: 当前的日程信息
Returns:
str: 生成的新的内心独白如果出错则返回提示信息
@ -58,7 +57,6 @@ class Mind:
# Format prompt
try:
prompt = (await global_prompt_manager.get_prompt_async("thinking_prompt")).format(
schedule_info=schedule_info,
personality_info=personality_info,
related_memory_info=related_memory_info,
current_thinking_info=current_main_mind, # Use passed current mind

View File

@ -94,6 +94,9 @@ class SubHeartflow:
await self.interest_chatting.initialize()
logger.debug(f"{self.log_prefix} InterestChatting 实例已初始化。")
# 确保普通聊天已启动
await self._start_normal_chat(rewind=True)
def update_last_chat_state_time(self):
self.chat_state_last_time = time.time() - self.chat_state_changed_time
@ -103,7 +106,7 @@ class SubHeartflow:
切出 CHAT 状态时使用
"""
if self.normal_chat_instance:
logger.info(f"{self.log_prefix} 离开CHAT模式结束 随便水群")
logger.info(f"{self.log_prefix} 结束随便水群")
try:
await self.normal_chat_instance.stop_chat() # 调用 stop_chat
except Exception as e:
@ -116,7 +119,7 @@ class SubHeartflow:
进入 CHAT 状态时使用
确保 HeartFChatting 已停止
"""
await self._stop_heart_fc_chat() # 确保 专注聊天已停止
await self._stop_heart_fc_chat() # 确保 专注水群已停止
log_prefix = self.log_prefix
try:
@ -128,7 +131,8 @@ class SubHeartflow:
if rewind:
self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict())
else:
self.normal_chat_instance = NormalChat(chat_stream=chat_stream)
self.clear_interest_dict()
self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict())
# 进行异步初始化
await self.normal_chat_instance.initialize()
@ -146,7 +150,7 @@ class SubHeartflow:
async def _stop_heart_fc_chat(self):
"""停止并清理 HeartFChatting 实例"""
if self.heart_fc_instance:
logger.debug(f"{self.log_prefix} 结束专注聊天...")
logger.debug(f"{self.log_prefix} 结束专注水群...")
try:
await self.heart_fc_instance.shutdown()
except Exception as e:
@ -159,7 +163,7 @@ class SubHeartflow:
async def _start_heart_fc_chat(self) -> bool:
"""启动 HeartFChatting 实例,确保 NormalChat 已停止"""
await self._stop_normal_chat() # 确保普通聊天监控已停止
self.clear_interest_dict() # 清理兴趣字典,准备专注聊天
self.clear_interest_dict() # 清理兴趣字典,准备专注水群
log_prefix = self.log_prefix
# 如果实例已存在,检查其循环任务状态
@ -181,7 +185,7 @@ class SubHeartflow:
return True # 已经在运行
# 如果实例不存在,则创建并启动
logger.info(f"{log_prefix} 麦麦准备开始专注聊天...")
logger.info(f"{log_prefix} 麦麦准备开始专注水群...")
try:
# 创建 HeartFChatting 实例,并传递 从构造函数传入的 回调函数
self.heart_fc_instance = HeartFChatting(
@ -194,7 +198,7 @@ class SubHeartflow:
# 初始化并启动 HeartFChatting
if await self.heart_fc_instance._initialize():
await self.heart_fc_instance.start()
logger.debug(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。")
logger.debug(f"{log_prefix} 麦麦已成功进入专注水群模式 (新实例已启动)。")
return True
else:
logger.error(f"{log_prefix} HeartFChatting 初始化失败,无法进入专注模式。")
@ -218,44 +222,30 @@ class SubHeartflow:
# --- 状态转换逻辑 ---
if new_state == ChatState.CHAT:
# 移除限额检查逻辑
logger.debug(f"{log_prefix} 准备进入或保持 聊天 状态")
if current_state == ChatState.FOCUSED:
if await self._start_normal_chat(rewind=False):
# logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。")
state_changed = True
else:
logger.error(f"{log_prefix} 从FOCUSED状态启动 NormalChat 失败,无法进入 CHAT 状态。")
# 考虑是否需要回滚状态或采取其他措施
return # 启动失败,不改变状态
else:
# 这个分支不应该再被进入因为已没有ABSENT状态
if await self._start_normal_chat(rewind=True):
# logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。")
state_changed = True
else:
logger.error(f"{log_prefix} 从ABSENT状态启动 NormalChat 失败,无法进入 CHAT 状态。")
# 考虑是否需要回滚状态或采取其他措施
logger.error(f"{log_prefix} 启动 NormalChat 失败,无法进入 CHAT 状态。")
return # 启动失败,不改变状态
elif new_state == ChatState.FOCUSED:
# 移除限额检查逻辑
logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态")
logger.debug(f"{log_prefix} 准备进入或保持 专注水群 状态")
if await self._start_heart_fc_chat():
logger.debug(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。")
state_changed = True
else:
logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。")
# 启动失败状态回滚到之前的状态或ABSENT这里保持不改变
return # 启动失败,不改变状态
elif new_state == ChatState.ABSENT:
logger.info(f"{log_prefix} 进入 ABSENT 状态,停止所有聊天活动...")
await self.clear_interest_dict()
await self._stop_normal_chat()
await self._stop_heart_fc_chat()
state_changed = True # 总是可以成功转换到 ABSENT
# --- 更新状态和最后活动时间 ---
if state_changed:
self.update_last_chat_state_time()
@ -369,6 +359,6 @@ class SubHeartflow:
logger.error(f"{self.log_prefix} 等待子心流主任务取消时发生错误 (Shutdown): {e}")
self.task = None # 清理任务引用
self.chat_state.chat_status = ChatState.ABSENT # 状态重置为不参与
self.chat_state.chat_status = ChatState.CHAT # 状态重置为随便水群而不是不参与
logger.info(f"{self.log_prefix} 子心流关闭完成。")

View File

@ -28,7 +28,6 @@ import traceback
logger = get_logger("subheartflow_manager")
# 子心流管理相关常量
INACTIVE_THRESHOLD_SECONDS = 3600 # 子心流不活跃超时时间(秒)
NORMAL_CHAT_TIMEOUT_SECONDS = 30 * 60 # 30分钟
@ -74,15 +73,6 @@ class SubHeartflowManager:
self._lock = asyncio.Lock() # 用于保护 self.subheartflows 的访问
self.mai_state_info: MaiStateInfo = mai_state_info # 存储传入的 MaiStateInfo 实例
# 为 LLM 状态评估创建一个 LLMRequest 实例
# 使用与 Heartflow 相同的模型和参数
self.llm_state_evaluator = LLMRequest(
model=global_config.llm_heartflow, # 与 Heartflow 一致
temperature=0.6, # 与 Heartflow 一致
max_tokens=1000, # 与 Heartflow 一致 (虽然可能不需要这么多)
request_type="subheartflow_state_eval", # 保留特定的请求类型
)
async def force_change_state(self, subflow_id: Any, target_state: ChatState) -> bool:
"""强制改变指定子心流的状态"""
async with self._lock:
@ -155,128 +145,9 @@ class SubHeartflowManager:
logger.error(f"创建子心流 {subheartflow_id} 失败: {e}", exc_info=True)
return None
# --- 新增:内部方法,用于尝试将单个子心流设置为 ABSENT ---
# --- 结束新增 ---
async def sleep_subheartflow(self, subheartflow_id: Any, reason: str) -> bool:
"""停止指定的子心流并将其状态设置为 ABSENT"""
log_prefix = "[子心流管理]"
async with self._lock: # 加锁以安全访问字典
subheartflow = self.subheartflows.get(subheartflow_id)
stream_name = chat_manager.get_stream_name(subheartflow_id) or subheartflow_id
logger.info(f"{log_prefix} 正在停止 {stream_name}, 原因: {reason}")
# 调用内部方法处理状态变更
success = await _try_set_subflow_absent_internal(subheartflow, log_prefix)
return success
# 锁在此处自动释放
def get_inactive_subheartflows(self, max_age_seconds=INACTIVE_THRESHOLD_SECONDS):
"""识别并返回需要清理的不活跃(处于ABSENT状态超过一小时)子心流(id, 原因)"""
_current_time = time.time()
flows_to_stop = []
for subheartflow_id, subheartflow in list(self.subheartflows.items()):
state = subheartflow.chat_state.chat_status
if state != ChatState.ABSENT:
continue
subheartflow.update_last_chat_state_time()
_absent_last_time = subheartflow.chat_state_last_time
flows_to_stop.append(subheartflow_id)
return flows_to_stop
async def enforce_subheartflow_limits(self):
"""根据主状态限制停止超额子心流(优先停不活跃的)"""
# 使用 self.mai_state_info 获取当前状态和限制
current_mai_state = self.mai_state_info.get_current_state()
normal_limit = current_mai_state.get_normal_chat_max_num()
focused_limit = current_mai_state.get_focused_chat_max_num()
logger.debug(f"[限制] 状态:{current_mai_state.value}, 普通限:{normal_limit}, 专注限:{focused_limit}")
# 分类统计当前子心流
normal_flows = []
focused_flows = []
for flow_id, flow in list(self.subheartflows.items()):
if flow.chat_state.chat_status == ChatState.CHAT:
normal_flows.append((flow_id, getattr(flow, "last_active_time", 0)))
elif flow.chat_state.chat_status == ChatState.FOCUSED:
focused_flows.append((flow_id, getattr(flow, "last_active_time", 0)))
logger.debug(f"[限制] 当前数量 - 普通:{len(normal_flows)}, 专注:{len(focused_flows)}")
stopped = 0
# 处理普通聊天超额
if len(normal_flows) > normal_limit:
excess = len(normal_flows) - normal_limit
logger.info(f"[限制] 普通聊天超额({len(normal_flows)}>{normal_limit}), 停止{excess}")
normal_flows.sort(key=lambda x: x[1])
for flow_id, _ in normal_flows[:excess]:
if await self.sleep_subheartflow(flow_id, f"普通聊天超额(限{normal_limit})"):
stopped += 1
# 处理专注聊天超额(需重新统计)
focused_flows = [
(fid, t)
for fid, f in list(self.subheartflows.items())
if (t := getattr(f, "last_active_time", 0)) and f.chat_state.chat_status == ChatState.FOCUSED
]
if len(focused_flows) > focused_limit:
excess = len(focused_flows) - focused_limit
logger.info(f"[限制] 专注聊天超额({len(focused_flows)}>{focused_limit}), 停止{excess}")
focused_flows.sort(key=lambda x: x[1])
for flow_id, _ in focused_flows[:excess]:
if await self.sleep_subheartflow(flow_id, f"专注聊天超额(限{focused_limit})"):
stopped += 1
if stopped:
logger.info(f"[限制] 已停止{stopped}个子心流, 剩余:{len(self.subheartflows)}")
else:
logger.debug(f"[限制] 无需停止, 当前总数:{len(self.subheartflows)}")
async def deactivate_all_subflows(self):
"""将所有子心流的状态更改为 ABSENT (例如主状态变为OFFLINE时调用)"""
log_prefix = "[停用]"
changed_count = 0
processed_count = 0
async with self._lock: # 获取锁以安全迭代
# 使用 list() 创建一个当前值的快照,防止在迭代时修改字典
flows_to_update = list(self.subheartflows.values())
processed_count = len(flows_to_update)
if not flows_to_update:
logger.debug(f"{log_prefix} 无活跃子心流,无需操作")
return
for subflow in flows_to_update:
# 记录原始状态,以便统计实际改变的数量
original_state_was_absent = subflow.chat_state.chat_status == ChatState.ABSENT
success = await _try_set_subflow_absent_internal(subflow, log_prefix)
# 如果成功设置为 ABSENT 且原始状态不是 ABSENT则计数
if success and not original_state_was_absent:
if subflow.chat_state.chat_status == ChatState.ABSENT:
changed_count += 1
else:
# 这种情况理论上不应发生,如果内部方法返回 True 的话
stream_name = chat_manager.get_stream_name(subflow.subheartflow_id) or subflow.subheartflow_id
logger.warning(f"{log_prefix} 内部方法声称成功但 {stream_name} 状态未变为 ABSENT。")
# 锁在此处自动释放
logger.info(
f"{log_prefix} 完成,共处理 {processed_count} 个子心流,成功将 {changed_count} 个非 ABSENT 子心流的状态更改为 ABSENT。"
)
async def sbhf_absent_into_focus(self):
"""评估子心流兴趣度满足条件且未达上限则提升到FOCUSED状态基于start_hfc_probability"""
async def sbhf_chat_into_focus(self):
"""评估子心流兴趣度满足条件则提升到FOCUSED状态基于start_hfc_probability"""
try:
current_state = self.mai_state_info.get_current_state()
focused_limit = current_state.get_focused_chat_max_num()
# --- 新增:检查是否允许进入 FOCUS 模式 --- #
if not global_config.allow_focus_mode:
if int(time.time()) % 60 == 0: # 每60秒输出一次日志避免刷屏
@ -284,22 +155,11 @@ class SubHeartflowManager:
return # 如果不允许,直接返回
# --- 结束新增 ---
logger.info(f"当前状态 ({current_state.value}) 可以在{focused_limit}个群 专注聊天")
if focused_limit <= 0:
# logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 不允许 FOCUSED 子心流")
return
current_focused_count = self.count_subflows_by_state(ChatState.FOCUSED)
if current_focused_count >= focused_limit:
logger.debug(f"已达专注上限 ({current_focused_count}/{focused_limit})")
return
for sub_hf in list(self.subheartflows.values()):
flow_id = sub_hf.subheartflow_id
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
# 跳过非CHAT状态或已经是FOCUSED状态的子心流
# 跳过已经是FOCUSED状态的子心流
if sub_hf.chat_state.chat_status == ChatState.FOCUSED:
continue
@ -310,21 +170,12 @@ class SubHeartflowManager:
f"{stream_name},现在状态: {sub_hf.chat_state.chat_status.value},进入专注概率: {sub_hf.interest_chatting.start_hfc_probability}"
)
# 调试用
from .mai_state_manager import enable_unlimited_hfc_chat
if not enable_unlimited_hfc_chat:
if sub_hf.chat_state.chat_status != ChatState.CHAT:
continue
if sub_hf.chat_state.chat_status != ChatState.CHAT:
continue
if random.random() >= sub_hf.interest_chatting.start_hfc_probability:
continue
# 再次检查是否达到上限
if current_focused_count >= focused_limit:
logger.debug(f"{stream_name} 已达专注上限")
break
# 获取最新状态并执行提升
current_subflow = self.subheartflows.get(flow_id)
if not current_subflow:
@ -336,284 +187,74 @@ class SubHeartflowManager:
# 执行状态提升
await current_subflow.change_chat_state(ChatState.FOCUSED)
# 验证提升结果
if (
final_subflow := self.subheartflows.get(flow_id)
) and final_subflow.chat_state.chat_status == ChatState.FOCUSED:
current_focused_count += 1
except Exception as e:
logger.error(f"启动HFC 兴趣评估失败: {e}", exc_info=True)
async def sbhf_absent_into_chat(self):
"""
随机选一个 ABSENT 状态的 *群聊* 子心流评估是否应转换为 CHAT 状态
每次调用最多转换一个
私聊会被忽略
"""
current_mai_state = self.mai_state_info.get_current_state()
chat_limit = current_mai_state.get_normal_chat_max_num()
async def sbhf_focus_into_chat(self):
"""检查FOCUSED状态的子心流是否需要转回CHAT状态"""
focus_time_limit = 600 # 专注状态最多持续10分钟
async with self._lock:
# 1. 筛选出所有 ABSENT 状态的 *群聊* 子心流
absent_group_subflows = [
hf
for hf in self.subheartflows.values()
if hf.chat_state.chat_status == ChatState.ABSENT and hf.is_group_chat
# 筛选出所有FOCUSED状态的子心流
focused_subflows = [
hf for hf in self.subheartflows.values()
if hf.chat_state.chat_status == ChatState.FOCUSED
]
if not absent_group_subflows:
# logger.debug("没有摸鱼的群聊子心流可以评估。") # 日志太频繁
return # 没有目标,直接返回
# 2. 随机选一个幸运儿
sub_hf_to_evaluate = random.choice(absent_group_subflows)
flow_id = sub_hf_to_evaluate.subheartflow_id
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
log_prefix = f"[{stream_name}]"
# 3. 检查 CHAT 上限
current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
if current_chat_count >= chat_limit:
logger.info(f"{log_prefix} 想看看能不能聊,但是聊天太多了, ({current_chat_count}/{chat_limit}) 满了。")
return # 满了,这次就算了
# --- 获取 FOCUSED 计数 ---
current_focused_count = self.count_subflows_by_state_nolock(ChatState.FOCUSED)
focused_limit = current_mai_state.get_focused_chat_max_num()
# --- 新增:获取聊天和专注群名 ---
chatting_group_names = []
focused_group_names = []
for flow_id, hf in self.subheartflows.items():
stream_name = chat_manager.get_stream_name(flow_id) or str(flow_id) # 保证有名字
if hf.chat_state.chat_status == ChatState.CHAT:
chatting_group_names.append(stream_name)
elif hf.chat_state.chat_status == ChatState.FOCUSED:
focused_group_names.append(stream_name)
# --- 结束新增 ---
# --- 获取观察信息和构建 Prompt ---
first_observation = sub_hf_to_evaluate.observations[0] # 喵~第一个观察者肯定存在的说
await first_observation.observe()
current_chat_log = first_observation.talking_message_str or "当前没啥聊天内容。"
_observation_summary = f"在[{stream_name}]这个群中,你最近看群友聊了这些:\n{current_chat_log}"
_mai_state_description = f"你当前状态: {current_mai_state.value}"
individuality = Individuality.get_instance()
personality_prompt = individuality.get_prompt(x_person=2, level=2)
prompt_personality = f"你正在扮演名为{individuality.name}的人类,{personality_prompt}"
# --- 修改:在 prompt 中加入当前聊天计数和群名信息 (条件显示) ---
chat_status_lines = []
if chatting_group_names:
chat_status_lines.append(
f"正在这些群闲聊 ({current_chat_count}/{chat_limit}): {', '.join(chatting_group_names)}"
)
if focused_group_names:
chat_status_lines.append(
f"正在这些群专注的聊天 ({current_focused_count}/{focused_limit}): {', '.join(focused_group_names)}"
)
chat_status_prompt = "当前没有在任何群聊中。" # 默认消息喵~
if chat_status_lines:
chat_status_prompt = "当前聊天情况,你已经参与了下面这几个群的聊天:\n" + "\n".join(
chat_status_lines
) # 拼接状态信息
prompt = (
f"{prompt_personality}\n"
f"{chat_status_prompt}\n" # <-- 喵!用了新的状态信息~
f"你当前尚未加入 [{stream_name}] 群聊天。\n"
f"{_observation_summary}\n---\n"
f"基于以上信息,你想不想开始在这个群闲聊?\n"
f"请说明理由,并以 JSON 格式回答,包含 'decision' (布尔值) 和 'reason' (字符串)。\n"
f'例如:{{"decision": true, "reason": "看起来挺热闹的,插个话"}}\n'
f'例如:{{"decision": false, "reason": "已经聊了好多,休息一下"}}\n'
f"请只输出有效的 JSON 对象。"
)
# --- 结束修改 ---
# --- 4. LLM 评估是否想聊 ---
yao_kai_shi_liao_ma, reason = await self._llm_evaluate_state_transition(prompt)
if reason:
if yao_kai_shi_liao_ma:
logger.info(f"{log_prefix} 打算开始聊,原因是: {reason}")
else:
logger.info(f"{log_prefix} 不打算聊,原因是: {reason}")
else:
logger.info(f"{log_prefix} 结果: {yao_kai_shi_liao_ma}")
if yao_kai_shi_liao_ma is None:
logger.debug(f"{log_prefix} 问AI想不想聊失败了这次算了。")
return # 评估失败,结束
if not yao_kai_shi_liao_ma:
# logger.info(f"{log_prefix} 现在不想聊这个群。")
return # 不想聊,结束
# --- 5. AI想聊再次检查额度并尝试转换 ---
# 再次检查以防万一
current_chat_count_before_change = self.count_subflows_by_state_nolock(ChatState.CHAT)
if current_chat_count_before_change < chat_limit:
logger.info(
f"{log_prefix} 想聊,而且还有精力 ({current_chat_count_before_change}/{chat_limit}),这就去聊!"
)
await sub_hf_to_evaluate.change_chat_state(ChatState.CHAT)
# 确认转换成功
if sub_hf_to_evaluate.chat_state.chat_status == ChatState.CHAT:
logger.debug(f"{log_prefix} 成功进入聊天状态!本次评估圆满结束。")
else:
logger.warning(
f"{log_prefix} 奇怪,尝试进入聊天状态失败了。当前状态: {sub_hf_to_evaluate.chat_state.chat_status.value}"
)
else:
logger.warning(
f"{log_prefix} AI说想聊但是刚问完就没空位了 ({current_chat_count_before_change}/{chat_limit})。真不巧,下次再说吧。"
)
# 无论转换成功与否,本次评估都结束了
# 锁在这里自动释放
# --- 新增:单独检查 CHAT 状态超时的任务 ---
async def sbhf_chat_into_absent(self):
"""定期检查处于 CHAT 状态的子心流是否因长时间未发言而超时,并将其转为 ABSENT。"""
log_prefix_task = "[聊天超时检查]"
transitioned_to_absent = 0
checked_count = 0
async with self._lock:
subflows_snapshot = list(self.subheartflows.values())
checked_count = len(subflows_snapshot)
if not subflows_snapshot:
if not focused_subflows:
return
for sub_hf in subflows_snapshot:
# 只检查 CHAT 状态的子心流
if sub_hf.chat_state.chat_status != ChatState.CHAT:
continue
for sub_hf in focused_subflows:
flow_id = sub_hf.subheartflow_id
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
log_prefix = f"[{stream_name}]({log_prefix_task})"
log_prefix = f"[{stream_name}]"
should_deactivate = False
reason = ""
# 检查持续时间
sub_hf.update_last_chat_state_time()
time_in_state = sub_hf.chat_state_last_time
# 10%概率随机转回CHAT或者超过时间限制
if time_in_state > focus_time_limit or random.random() < 0.2:
logger.info(f"{log_prefix} {'超过时间限制' if time_in_state > focus_time_limit else '随机'}从专注水群转为普通聊天")
await sub_hf.change_chat_state(ChatState.CHAT)
# --- 新增:处理 HFC 无回复回调的专用方法 --- #
async def _handle_hfc_no_reply(self, subheartflow_id: Any):
"""处理来自 HeartFChatting 的连续无回复信号 (通过 partial 绑定 ID)"""
logger.debug(f"[管理器 HFC 处理器] 接收到来自 {subheartflow_id} 的 HFC 无回复信号")
await self.sbhf_focus_into_chat_single(subheartflow_id)
# --- 专用于处理单个子心流从FOCUSED转为CHAT的方法 --- #
async def sbhf_focus_into_chat_single(self, subflow_id: Any):
"""将特定的FOCUSED状态子心流转为CHAT状态"""
async with self._lock:
subflow = self.subheartflows.get(subflow_id)
if not subflow:
logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id}")
return
stream_name = chat_manager.get_stream_name(subflow_id) or subflow_id
current_state = subflow.chat_state.chat_status
if current_state == ChatState.FOCUSED:
logger.info(f"[状态转换请求] 将 {stream_name} (当前: {current_state.value}) 转换为 CHAT")
try:
last_bot_dong_zuo_time = sub_hf.get_normal_chat_last_speak_time()
if last_bot_dong_zuo_time > 0:
current_time = time.time()
time_since_last_bb = current_time - last_bot_dong_zuo_time
minutes_since_last_bb = time_since_last_bb / 60
# 60分钟强制退出
if minutes_since_last_bb >= 60:
should_deactivate = True
reason = "超过60分钟未发言强制退出"
else:
# 根据时间区间确定退出概率
exit_probability = 0
if minutes_since_last_bb < 5:
exit_probability = 0.01 # 1%
elif minutes_since_last_bb < 15:
exit_probability = 0.02 # 2%
elif minutes_since_last_bb < 30:
exit_probability = 0.04 # 4%
else:
exit_probability = 0.08 # 8%
# 随机判断是否退出
if random.random() < exit_probability:
should_deactivate = True
reason = f"{minutes_since_last_bb:.1f}分钟未发言,触发{exit_probability * 100:.0f}%退出概率"
except AttributeError:
logger.error(
f"{log_prefix} 无法获取 Bot 最后 BB 时间,请确保 SubHeartflow 相关实现正确。跳过超时检查。"
)
except Exception as e:
logger.error(f"{log_prefix} 检查 Bot 超时状态时出错: {e}", exc_info=True)
# 执行状态转换(如果超时)
if should_deactivate:
logger.debug(f"{log_prefix} 因超时 ({reason}),尝试转换为 ABSENT 状态。")
await sub_hf.change_chat_state(ChatState.ABSENT)
# 再次检查确保状态已改变
if sub_hf.chat_state.chat_status == ChatState.ABSENT:
transitioned_to_absent += 1
logger.info(f"{log_prefix} 不看了。")
# 从HFC到CHAT时清空兴趣字典
subflow.clear_interest_dict()
await subflow.change_chat_state(ChatState.CHAT)
final_state = subflow.chat_state.chat_status
if final_state == ChatState.CHAT:
logger.debug(f"[状态转换请求] {stream_name} 状态已成功转换为 {final_state.value}")
else:
logger.warning(f"{log_prefix} 尝试因超时转换为 ABSENT 失败。")
if transitioned_to_absent > 0:
logger.debug(
f"{log_prefix_task} 完成,共检查 {checked_count} 个子心流,{transitioned_to_absent} 个因超时转为 ABSENT。"
)
# --- 结束新增 ---
async def _llm_evaluate_state_transition(self, prompt: str) -> Tuple[Optional[bool], Optional[str]]:
"""
使用 LLM 评估是否应进行状态转换期望 LLM 返回 JSON 格式
Args:
prompt: 提供给 LLM 的提示信息要求返回 {"decision": true/false}
Returns:
Optional[bool]: 如果成功解析 LLM JSON 响应并提取了 'decision' 键的值则返回该布尔值
如果 LLM 调用失败返回无效 JSON JSON 中缺少 'decision' 键或其值不是布尔型则返回 None
"""
log_prefix = "[LLM状态评估]"
try:
# --- 真实的 LLM 调用 ---
response_text, _ = await self.llm_state_evaluator.generate_response_async(prompt)
# logger.debug(f"{log_prefix} 使用模型 {self.llm_state_evaluator.model_name} 评估")
logger.debug(f"{log_prefix} 原始输入: {prompt}")
logger.debug(f"{log_prefix} 原始评估结果: {response_text}")
# --- 解析 JSON 响应 ---
try:
# 尝试去除可能的Markdown代码块标记
cleaned_response = response_text.strip().strip("`").strip()
if cleaned_response.startswith("json"):
cleaned_response = cleaned_response[4:].strip()
data = json.loads(cleaned_response)
decision = data.get("decision") # 使用 .get() 避免 KeyError
reason = data.get("reason")
if isinstance(decision, bool):
logger.debug(f"{log_prefix} LLM评估结果 (来自JSON): {'建议转换' if decision else '建议不转换'}")
return decision, reason
else:
logger.warning(
f"{log_prefix} LLM 返回的 JSON 中 'decision' 键的值不是布尔型: {decision}。响应: {response_text}"
logger.warning(
f"[状态转换请求] 尝试将 {stream_name} 转换为 CHAT 后,状态实际为 {final_state.value}"
)
except Exception as e:
logger.error(
f"[状态转换请求] 转换 {stream_name} 到 CHAT 时出错: {e}", exc_info=True
)
return None, None # 值类型不正确
except json.JSONDecodeError as json_err:
logger.warning(f"{log_prefix} LLM 返回的响应不是有效的 JSON: {json_err}。响应: {response_text}")
# 尝试在非JSON响应中查找关键词作为后备方案 (可选)
if "true" in response_text.lower():
logger.debug(f"{log_prefix} 在非JSON响应中找到 'true',解释为建议转换")
return True, None
if "false" in response_text.lower():
logger.debug(f"{log_prefix} 在非JSON响应中找到 'false',解释为建议不转换")
return False, None
return None, None # JSON 解析失败,也未找到关键词
except Exception as parse_err: # 捕获其他可能的解析错误
logger.warning(f"{log_prefix} 解析 LLM JSON 响应时发生意外错误: {parse_err}。响应: {response_text}")
return None, None
except Exception as e:
logger.error(f"{log_prefix} 调用 LLM 或处理其响应时出错: {e}", exc_info=True)
traceback.print_exc()
return None, None # LLM 调用或处理失败
else:
logger.debug(f"[状态转换请求] {stream_name} 已处于 CHAT 状态,无需转换")
def count_subflows_by_state(self, state: ChatState) -> int:
"""统计指定状态的子心流数量"""
@ -637,12 +278,10 @@ class SubHeartflowManager:
return count
def get_active_subflow_minds(self) -> List[str]:
"""获取所有活跃(非ABSENT)子心流的当前想法"""
"""获取所有子心流的当前想法"""
minds = []
for subheartflow in self.subheartflows.values():
# 检查子心流是否活跃(非ABSENT状态)
if subheartflow.chat_state.chat_status != ChatState.ABSENT:
minds.append(subheartflow.sub_mind.current_mind)
minds.append(subheartflow.sub_mind.current_mind)
return minds
def update_main_mind_in_subflows(self, main_mind: str):
@ -668,182 +307,3 @@ class SubHeartflowManager:
logger.error(f"删除 SubHeartflow {subheartflow_id} 时出错: {e}", exc_info=True)
else:
logger.warning(f"尝试删除不存在的 SubHeartflow: {subheartflow_id}")
# --- 新增:处理 HFC 无回复回调的专用方法 --- #
async def _handle_hfc_no_reply(self, subheartflow_id: Any):
"""处理来自 HeartFChatting 的连续无回复信号 (通过 partial 绑定 ID)"""
# 注意:这里不需要再获取锁,因为 sbhf_focus_into_absent 内部会处理锁
logger.debug(f"[管理器 HFC 处理器] 接收到来自 {subheartflow_id} 的 HFC 无回复信号")
await self.sbhf_focus_into_absent_or_chat(subheartflow_id)
# --- 结束新增 --- #
# --- 新增:处理来自 HeartFChatting 的状态转换请求 --- #
async def sbhf_focus_into_absent_or_chat(self, subflow_id: Any):
"""
接收来自 HeartFChatting 的请求将特定子心流的状态转换为 ABSENT CHAT
通常在连续多次 "no_reply" 后被调用
对于私聊总是转换为 ABSENT
对于群聊随机决定转换为 ABSENT CHAT (如果 CHAT 未达上限)
Args:
subflow_id: 需要转换状态的子心流 ID
"""
async with self._lock:
subflow = self.subheartflows.get(subflow_id)
if not subflow:
logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 ABSENT/CHAT")
return
stream_name = chat_manager.get_stream_name(subflow_id) or subflow_id
current_state = subflow.chat_state.chat_status
if current_state == ChatState.FOCUSED:
target_state = ChatState.ABSENT # Default target
log_reason = "默认转换 (私聊或群聊)"
# --- Modify logic based on chat type --- #
if subflow.is_group_chat:
# Group chat: Decide between ABSENT or CHAT
if random.random() < 0.5: # 50% chance to try CHAT
current_mai_state = self.mai_state_info.get_current_state()
chat_limit = current_mai_state.get_normal_chat_max_num()
current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
if current_chat_count < chat_limit:
target_state = ChatState.CHAT
log_reason = f"群聊随机选择 CHAT (当前 {current_chat_count}/{chat_limit})"
else:
target_state = ChatState.ABSENT # Fallback to ABSENT if CHAT limit reached
log_reason = (
f"群聊随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT"
)
else: # 50% chance to go directly to ABSENT
target_state = ChatState.ABSENT
log_reason = "群聊随机选择 ABSENT"
else:
# Private chat: Always go to ABSENT
target_state = ChatState.ABSENT
log_reason = "私聊退出 FOCUSED转为 ABSENT"
# --- End modification --- #
logger.info(
f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 尝试转换为 {target_state.value} ({log_reason})"
)
try:
# 从HFC到CHAT时清空兴趣字典
subflow.clear_interest_dict()
await subflow.change_chat_state(target_state)
final_state = subflow.chat_state.chat_status
if final_state == target_state:
logger.debug(f"[状态转换请求] {stream_name} 状态已成功转换为 {final_state.value}")
else:
logger.warning(
f"[状态转换请求] 尝试将 {stream_name} 转换为 {target_state.value} 后,状态实际为 {final_state.value}"
)
except Exception as e:
logger.error(
f"[状态转换请求] 转换 {stream_name}{target_state.value} 时出错: {e}", exc_info=True
)
elif current_state == ChatState.ABSENT:
logger.debug(f"[状态转换请求] {stream_name} 已处于 ABSENT 状态,无需转换")
else:
logger.warning(
f"[状态转换请求] 收到对 {stream_name} 的请求,但其状态为 {current_state.value} (非 FOCUSED),不执行转换"
)
# --- 结束新增 --- #
# --- 新增:处理私聊从 ABSENT 直接到 FOCUSED 的逻辑 --- #
async def sbhf_absent_private_into_focus(self):
"""检查 ABSENT 状态的私聊子心流是否有新活动,若有且未达 FOCUSED 上限,则直接转换为 FOCUSED。"""
log_prefix_task = "[私聊激活检查]"
transitioned_count = 0
checked_count = 0
# --- 获取当前状态和 FOCUSED 上限 --- #
current_mai_state = self.mai_state_info.get_current_state()
focused_limit = current_mai_state.get_focused_chat_max_num()
# --- 检查是否允许 FOCUS 模式 --- #
if not global_config.allow_focus_mode:
# Log less frequently to avoid spam
# if int(time.time()) % 60 == 0:
# logger.debug(f"{log_prefix_task} 配置不允许进入 FOCUSED 状态")
return
if focused_limit <= 0:
# logger.debug(f"{log_prefix_task} 当前状态 ({current_mai_state.value}) 不允许 FOCUSED 子心流")
return
async with self._lock:
# --- 获取当前 FOCUSED 计数 (不上锁版本) --- #
current_focused_count = self.count_subflows_by_state_nolock(ChatState.FOCUSED)
# --- 筛选出所有 ABSENT 状态的私聊子心流 --- #
eligible_subflows = [
hf
for hf in self.subheartflows.values()
if hf.chat_state.chat_status == ChatState.ABSENT and not hf.is_group_chat
]
checked_count = len(eligible_subflows)
if not eligible_subflows:
# logger.debug(f"{log_prefix_task} 没有 ABSENT 状态的私聊子心流可以评估。")
return
# --- 遍历评估每个符合条件的私聊 --- #
for sub_hf in eligible_subflows:
# --- 再次检查 FOCUSED 上限,因为可能有多个同时激活 --- #
if current_focused_count >= focused_limit:
logger.debug(
f"{log_prefix_task} 已达专注上限 ({current_focused_count}/{focused_limit}),停止检查后续私聊。"
)
break # 已满,无需再检查其他私聊
flow_id = sub_hf.subheartflow_id
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
log_prefix = f"[{stream_name}]({log_prefix_task})"
try:
# --- 检查是否有新活动 --- #
observation = sub_hf._get_primary_observation() # 获取主要观察者
is_active = False
if observation:
# 检查自上次状态变为 ABSENT 后是否有新消息
# 使用 chat_state_changed_time 可能更精确
# 加一点点缓冲时间(例如 1 秒)以防时间戳完全相等
timestamp_to_check = sub_hf.chat_state_changed_time - 1
has_new = await observation.has_new_messages_since(timestamp_to_check)
if has_new:
is_active = True
logger.debug(f"{log_prefix} 检测到新消息,标记为活跃。")
else:
logger.warning(f"{log_prefix} 无法获取主要观察者来检查活动状态。")
# --- 如果活跃且未达上限,则尝试转换 --- #
if is_active:
logger.info(
f"{log_prefix} 检测到活跃且未达专注上限 ({current_focused_count}/{focused_limit}),尝试转换为 FOCUSED。"
)
await sub_hf.change_chat_state(ChatState.FOCUSED)
# 确认转换成功
if sub_hf.chat_state.chat_status == ChatState.FOCUSED:
transitioned_count += 1
current_focused_count += 1 # 更新计数器以供本轮后续检查
logger.info(f"{log_prefix} 成功进入 FOCUSED 状态。")
else:
logger.warning(
f"{log_prefix} 尝试进入 FOCUSED 状态失败。当前状态: {sub_hf.chat_state.chat_status.value}"
)
# else: # 不活跃,无需操作
# logger.debug(f"{log_prefix} 未检测到新活动,保持 ABSENT。")
except Exception as e:
logger.error(f"{log_prefix} 检查私聊活动或转换状态时出错: {e}", exc_info=True)
# --- 循环结束后记录总结日志 --- #
if transitioned_count > 0:
logger.debug(
f"{log_prefix_task} 完成,共检查 {checked_count} 个私聊,{transitioned_count} 个转换为 FOCUSED。"
)

View File

@ -2,7 +2,6 @@ import asyncio
import time
from .plugins.utils.statistic import LLMStatistics
from .plugins.moods.moods import MoodManager
from .plugins.schedule.schedule_generator import bot_schedule
from .plugins.emoji_system.emoji_manager import emoji_manager
from .plugins.person_info.person_info import person_info_manager
from .plugins.willing.willing_manager import willing_manager
@ -81,14 +80,7 @@ class MainSystem:
self.hippocampus_manager.initialize(global_config=global_config)
# await asyncio.sleep(0.5) #防止logger输出飞了
# 初始化日程
bot_schedule.initialize(
name=global_config.BOT_NICKNAME,
personality=global_config.personality_core,
behavior=global_config.PROMPT_SCHEDULE_GEN,
interval=global_config.SCHEDULE_DOING_UPDATE_INTERVAL,
)
asyncio.create_task(bot_schedule.mai_schedule_start())
# 将bot.py中的chat_bot.message_process消息处理函数注册到api.py的消息处理基类中
self.app.register_message_handler(chat_bot.message_process)

View File

@ -8,7 +8,6 @@ from .emoji_system.emoji_manager import emoji_manager
from .person_info.relationship_manager import relationship_manager
from .moods.moods import MoodManager
from .willing.willing_manager import willing_manager
from .schedule.schedule_generator import bot_schedule
# 导出主要组件供外部使用
__all__ = [
@ -17,5 +16,4 @@ __all__ = [
"relationship_manager",
"MoodManager",
"willing_manager",
"bot_schedule",
]

View File

@ -32,7 +32,7 @@ from rich.traceback import install
install(extra_lines=3)
WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒
WAITING_TIME_THRESHOLD = 200 # 等待新消息时间阈值,单位秒
EMOJI_SEND_PRO = 0.3 # 设置一个概率,比如 30% 才真的发
@ -667,7 +667,7 @@ class HeartFChatting:
)
# 检查是否同时达到次数和时间阈值
time_threshold = 0.66 * WAITING_TIME_THRESHOLD * CONSECUTIVE_NO_REPLY_THRESHOLD
time_threshold = 0.5 * WAITING_TIME_THRESHOLD * CONSECUTIVE_NO_REPLY_THRESHOLD
if (
self._lian_xu_bu_hui_fu_ci_shu >= CONSECUTIVE_NO_REPLY_THRESHOLD
and self._lian_xu_deng_dai_shi_jian >= time_threshold

View File

@ -1,6 +1,6 @@
# HeartFChatting 逻辑详解
`HeartFChatting` 类是心流系统Heart Flow System中实现**专注聊天**`ChatState.FOCUSED`)功能的核心。顾名思义,其职责乃是在特定聊天流(`stream_id`)中,模拟更为连贯深入之对话。此非凭空臆造,而是依赖一个持续不断的 **思考(Think)-规划(Plan)-执行(Execute)** 循环。当其所系的 `SubHeartflow` 进入 `FOCUSED` 状态时,便会创建并启动 `HeartFChatting` 实例;若状态转为他途(譬如 `CHAT``ABSENT`),则会将其关闭。
`HeartFChatting` 类是心流系统Heart Flow System中实现**专注水群**`ChatState.FOCUSED`)功能的核心。顾名思义,其职责乃是在特定聊天流(`stream_id`)中,模拟更为连贯深入之对话。此非凭空臆造,而是依赖一个持续不断的 **思考(Think)-规划(Plan)-执行(Execute)** 循环。当其所系的 `SubHeartflow` 进入 `FOCUSED` 状态时,便会创建并启动 `HeartFChatting` 实例;若状态转为他途(譬如 `CHAT``ABSENT`),则会将其关闭。
## 1. 初始化简述 (`__init__`, `_initialize`)
@ -89,4 +89,4 @@
## 总结
`HeartFChatting` 通过 **观察 -> 思考(含工具)-> 规划 -> 执行** 的闭环,并利用 `CycleInfo` 进行上下文传递,实现了更加智能和连贯的专注聊天行为。其核心在于利用 `SubMind` 进行深度思考和信息收集,再通过 LLM 规划器进行决策,最后由 `HeartFCSender` 可靠地执行消息发送任务。
`HeartFChatting` 通过 **观察 -> 思考(含工具)-> 规划 -> 执行** 的闭环,并利用 `CycleInfo` 进行上下文传递,实现了更加智能和连贯的专注水群行为。其核心在于利用 `SubMind` 进行深度思考和信息收集,再通过 LLM 规划器进行决策,最后由 `HeartFCSender` 可靠地执行消息发送任务。

View File

@ -12,7 +12,6 @@ from ...common.database import db
from ..chat.utils import get_recent_group_speaker
from ..moods.moods import MoodManager
from ..memory_system.Hippocampus import HippocampusManager
from ..schedule.schedule_generator import bot_schedule
from ..knowledge.knowledge_lib import qa_manager
import traceback
from .heartFC_Cycleinfo import CycleInfo
@ -117,7 +116,6 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
{memory_prompt}
{relation_prompt}
{prompt_info}
{schedule_prompt}
{chat_target}
{chat_talking_prompt}
现在"{sender_name}"说的:{message_txt}引起了你的注意你想要在群里发言或者回复这条消息\n
@ -135,7 +133,6 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
"你回忆起:{related_memory_info}\n以上是你的回忆,不一定是目前聊天里的人说的,也不一定是现在发生的事情,请记住。\n",
"memory_prompt",
)
Prompt("你现在正在做的事情是:{schedule_info}", "schedule_prompt")
Prompt("\n你有以下这些**知识**\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt")
# --- Template for HeartFChatting (FOCUSED mode) ---
@ -166,7 +163,6 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
{memory_prompt}
{relation_prompt}
{prompt_info}
{schedule_prompt}
你正在和 {sender_name} 私聊
聊天记录如下
{chat_talking_prompt}
@ -426,13 +422,6 @@ class PromptBuilder:
end_time = time.time()
logger.debug(f"知识检索耗时: {(end_time - start_time):.3f}")
if global_config.ENABLE_SCHEDULE_GEN:
schedule_prompt = await global_prompt_manager.format_prompt(
"schedule_prompt", schedule_info=bot_schedule.get_current_num_task(num=1, time_info=False)
)
else:
schedule_prompt = ""
logger.debug("开始构建 normal prompt")
# --- Choose template and format based on chat type ---
@ -448,7 +437,6 @@ class PromptBuilder:
sender_name=effective_sender_name,
memory_prompt=memory_prompt,
prompt_info=prompt_info,
schedule_prompt=schedule_prompt,
chat_target=chat_target_1,
chat_target_2=chat_target_2,
chat_talking_prompt=chat_talking_prompt,
@ -473,7 +461,6 @@ class PromptBuilder:
sender_name=effective_sender_name,
memory_prompt=memory_prompt,
prompt_info=prompt_info,
schedule_prompt=schedule_prompt,
chat_talking_prompt=chat_talking_prompt,
message_txt=message_txt,
bot_name=global_config.BOT_NICKNAME,

View File

@ -22,7 +22,7 @@ from src.plugins.utils.timer_calculator import Timer
from src.heart_flow.utils_chat import get_chat_type_and_target_info
logger = get_logger("chat")
logger = get_logger("normal_chat")
class NormalChat:
@ -201,12 +201,10 @@ class NormalChat:
break
# 获取待处理消息列表
if self.interest_dict:
items_to_process = list(self.interest_dict.items())
if not items_to_process:
continue
else:
return
items_to_process = list(self.interest_dict.items())
if not items_to_process:
continue
# 处理每条兴趣消息
for msg_id, (message, interest_value, is_mentioned) in items_to_process:
@ -489,7 +487,7 @@ class NormalChat:
finally:
if self._chat_task is task:
self._chat_task = None
logger.debug(f"[{self.stream_name}] 任务清理完成")
logger.debug(f"[{self.stream_name}] Normal_chat 结束")
# 改为实例方法, 移除 stream_id 参数
async def stop_chat(self):

View File

@ -157,7 +157,7 @@ class LLMRequest:
completion_tokens: 输出token数
total_tokens: 总token数
user_id: 用户ID默认为system
request_type: 请求类型(chat/embedding/image/topic/schedule)
request_type: 请求类型
endpoint: API端点
"""
# 如果 request_type 为 None则使用实例变量中的值

View File

@ -1,307 +0,0 @@
import datetime
import os
import sys
import asyncio
from dateutil import tz
# 添加项目根目录到 Python 路径
root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
sys.path.append(root_path)
from src.common.database import db # noqa: E402
from src.common.logger import get_module_logger, SCHEDULE_STYLE_CONFIG, LogConfig # noqa: E402
from src.plugins.models.utils_model import LLMRequest # noqa: E402
from src.config.config import global_config # noqa: E402
TIME_ZONE = tz.gettz(global_config.TIME_ZONE) # 设置时区
schedule_config = LogConfig(
# 使用海马体专用样式
console_format=SCHEDULE_STYLE_CONFIG["console_format"],
file_format=SCHEDULE_STYLE_CONFIG["file_format"],
)
logger = get_module_logger("scheduler", config=schedule_config)
class ScheduleGenerator:
# enable_output: bool = True
def __init__(self):
# 使用离线LLM模型
self.enable_output = None
self.llm_scheduler_all = LLMRequest(
model=global_config.llm_reasoning,
temperature=global_config.SCHEDULE_TEMPERATURE + 0.3,
max_tokens=7000,
request_type="schedule",
)
self.llm_scheduler_doing = LLMRequest(
model=global_config.llm_normal,
temperature=global_config.SCHEDULE_TEMPERATURE,
max_tokens=2048,
request_type="schedule",
)
self.today_schedule_text = ""
self.today_done_list = []
self.yesterday_schedule_text = ""
self.yesterday_done_list = []
self.name = ""
self.personality = ""
self.behavior = ""
self.start_time = datetime.datetime.now(TIME_ZONE)
self.schedule_doing_update_interval = 300 # 最好大于60
def initialize(
self,
name: str = "bot_name",
personality: str = "你是一个爱国爱党的新时代青年",
behavior: str = "你非常外向,喜欢尝试新事物和人交流",
interval: int = 60,
):
"""初始化日程系统"""
self.name = name
self.behavior = behavior
self.schedule_doing_update_interval = interval
self.personality = personality
async def mai_schedule_start(self):
"""启动日程系统每5分钟执行一次move_doing并在日期变化时重新检查日程"""
try:
if global_config.ENABLE_SCHEDULE_GEN:
logger.info(f"日程系统启动/刷新时间: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}")
# 初始化日程
await self.check_and_create_today_schedule()
# self.print_schedule()
while True:
# print(self.get_current_num_task(1, True))
current_time = datetime.datetime.now(TIME_ZONE)
# 检查是否需要重新生成日程(日期变化)
if current_time.date() != self.start_time.date():
logger.info("检测到日期变化,重新生成日程")
self.start_time = current_time
await self.check_and_create_today_schedule()
# self.print_schedule()
# 执行当前活动
# mind_thinking = heartflow.current_state.current_mind
await self.move_doing()
await asyncio.sleep(self.schedule_doing_update_interval)
else:
logger.info("日程系统未启用")
except Exception as e:
logger.error(f"日程系统运行时出错: {str(e)}")
logger.exception("详细错误信息:")
async def check_and_create_today_schedule(self):
"""检查昨天的日程,并确保今天有日程安排
Returns:
tuple: (today_schedule_text, today_schedule) 今天的日程文本和解析后的日程字典
"""
today = datetime.datetime.now(TIME_ZONE)
yesterday = today - datetime.timedelta(days=1)
# 先检查昨天的日程
self.yesterday_schedule_text, self.yesterday_done_list = self.load_schedule_from_db(yesterday)
if self.yesterday_schedule_text:
logger.debug(f"已加载{yesterday.strftime('%Y-%m-%d')}的日程")
# 检查今天的日程
self.today_schedule_text, self.today_done_list = self.load_schedule_from_db(today)
if not self.today_done_list:
self.today_done_list = []
if not self.today_schedule_text:
logger.info(f"{today.strftime('%Y-%m-%d')}的日程不存在,准备生成新的日程")
try:
self.today_schedule_text = await self.generate_daily_schedule(target_date=today)
except Exception as e:
logger.error(f"生成日程时发生错误: {str(e)}")
self.today_schedule_text = ""
self.save_today_schedule_to_db()
def construct_daytime_prompt(self, target_date: datetime.datetime):
date_str = target_date.strftime("%Y-%m-%d")
weekday = target_date.strftime("%A")
prompt = f"你是{self.name}{self.personality}{self.behavior}"
prompt += f"你昨天的日程是:{self.yesterday_schedule_text}\n"
prompt += f"请为你生成{date_str}{weekday}),也就是今天的日程安排,结合你的个人特点和行为习惯以及昨天的安排\n"
prompt += "推测你的日程安排包括你一天都在做什么从起床到睡眠有什么发现和思考具体一些详细一些需要1500字以上精确到每半个小时记得写明时间\n" # noqa: E501
prompt += "直接返回你的日程,现实一点,不要浮夸,从起床到睡觉,不要输出其他内容:"
return prompt
def construct_doing_prompt(self, time: datetime.datetime, mind_thinking: str = ""):
now_time = time.strftime("%H:%M")
previous_doings = self.get_current_num_task(5, True)
prompt = f"你是{self.name}{self.personality}{self.behavior}"
prompt += f"你今天的日程是:{self.today_schedule_text}\n"
if previous_doings:
prompt += f"你之前做了的事情是:{previous_doings},从之前到现在已经过去了{self.schedule_doing_update_interval / 60}分钟了\n" # noqa: E501
if mind_thinking:
prompt += f"你脑子里在想:{mind_thinking}\n"
prompt += f"现在是{now_time},结合你的个人特点和行为习惯,注意关注你今天的日程安排和想法安排你接下来做什么,现实一点,不要浮夸"
prompt += "安排你接下来做什么,具体一些,详细一些\n"
prompt += "直接返回你在做的事情,注意是当前时间,不要输出其他内容:"
return prompt
async def generate_daily_schedule(
self,
target_date: datetime.datetime = None,
) -> dict[str, str]:
daytime_prompt = self.construct_daytime_prompt(target_date)
daytime_response, _ = await self.llm_scheduler_all.generate_response_async(daytime_prompt)
return daytime_response
def print_schedule(self):
"""打印完整的日程安排"""
if not self.today_schedule_text:
logger.warning("今日日程有误,将在下次运行时重新生成")
db.schedule.delete_one({"date": datetime.datetime.now(TIME_ZONE).strftime("%Y-%m-%d")})
else:
logger.info("=== 今日日程安排 ===")
logger.info(self.today_schedule_text)
logger.info("==================")
self.enable_output = False
async def update_today_done_list(self):
# 更新数据库中的 today_done_list
today_str = datetime.datetime.now(TIME_ZONE).strftime("%Y-%m-%d")
existing_schedule = db.schedule.find_one({"date": today_str})
if existing_schedule:
# 更新数据库中的 today_done_list
db.schedule.update_one({"date": today_str}, {"$set": {"today_done_list": self.today_done_list}})
logger.debug(f"已更新{today_str}的已完成活动列表")
else:
logger.warning(f"未找到{today_str}的日程记录")
async def move_doing(self, mind_thinking: str = ""):
try:
current_time = datetime.datetime.now(TIME_ZONE)
if mind_thinking:
doing_prompt = self.construct_doing_prompt(current_time, mind_thinking)
else:
doing_prompt = self.construct_doing_prompt(current_time)
doing_response, _ = await self.llm_scheduler_doing.generate_response_async(doing_prompt)
self.today_done_list.append((current_time, doing_response))
await self.update_today_done_list()
logger.info(f"当前活动: {doing_response}")
return doing_response
except GeneratorExit:
logger.warning("日程生成被中断")
return "日程生成被中断"
except Exception as e:
logger.error(f"生成日程时发生错误: {str(e)}")
return "生成日程时发生错误"
async def get_task_from_time_to_time(self, start_time: str, end_time: str):
"""获取指定时间范围内的任务列表
Args:
start_time (str): 开始时间格式为"HH:MM"
end_time (str): 结束时间格式为"HH:MM"
Returns:
list: 时间范围内的任务列表
"""
result = []
for task in self.today_done_list:
task_time = task[0] # 获取任务的时间戳
task_time_str = task_time.strftime("%H:%M")
# 检查任务时间是否在指定范围内
if self._time_diff(start_time, task_time_str) >= 0 and self._time_diff(task_time_str, end_time) >= 0:
result.append(task)
return result
def get_current_num_task(self, num=1, time_info=False):
"""获取最新加入的指定数量的日程
Args:
num (int): 需要获取的日程数量默认为1
time_info (bool): 是否包含时间信息默认为False
Returns:
list: 最新加入的日程列表
"""
if not self.today_done_list:
return []
# 确保num不超过列表长度
num = min(num, len(self.today_done_list))
pre_doings = ""
for doing in self.today_done_list[-num:]:
if time_info:
time_str = doing[0].strftime("%H:%M")
pre_doings += time_str + "时," + doing[1] + "\n"
else:
pre_doings += doing[1] + "\n"
# 返回最新的num条日程
return pre_doings
def save_today_schedule_to_db(self):
"""保存日程到数据库,同时初始化 today_done_list"""
date_str = datetime.datetime.now(TIME_ZONE).strftime("%Y-%m-%d")
schedule_data = {
"date": date_str,
"schedule": self.today_schedule_text,
"today_done_list": self.today_done_list if hasattr(self, "today_done_list") else [],
}
# 使用 upsert 操作,如果存在则更新,不存在则插入
db.schedule.update_one({"date": date_str}, {"$set": schedule_data}, upsert=True)
logger.debug(f"已保存{date_str}的日程到数据库")
@staticmethod
def load_schedule_from_db(date: datetime.datetime):
"""从数据库加载日程,同时加载 today_done_list"""
date_str = date.strftime("%Y-%m-%d")
existing_schedule = db.schedule.find_one({"date": date_str})
if existing_schedule:
schedule_text = existing_schedule["schedule"]
return schedule_text, existing_schedule.get("today_done_list", [])
else:
logger.debug(f"{date_str}的日程不存在")
return None, None
async def main():
# 使用示例
scheduler = ScheduleGenerator()
scheduler.initialize(
name="麦麦",
personality="你叫麦麦你19岁是一个大二的女大学生你有一头黑色短发你会刷贴吧你现在在学习心理学",
behavior="你比较内向一般熬夜比较晚然后第二天早上10点起床吃早午饭",
interval=60,
)
await scheduler.mai_schedule_start()
# 当作为组件导入时使用的实例
bot_schedule = ScheduleGenerator()
if __name__ == "__main__":
import asyncio
# 当直接运行此文件时执行
asyncio.run(main())

View File

@ -1,134 +0,0 @@
[inner.version]
describe = "版本号"
important = true
can_edit = false
[bot.qq]
describe = "机器人的QQ号"
important = true
can_edit = true
[bot.nickname]
describe = "机器人的昵称"
important = true
can_edit = true
[bot.alias_names]
describe = "机器人的别名列表,该选项还在调试中,暂时未生效"
important = false
can_edit = true
[groups.talk_allowed]
describe = "可以回复消息的群号码列表"
important = true
can_edit = true
[groups.talk_frequency_down]
describe = "降低回复频率的群号码列表"
important = false
can_edit = true
[groups.ban_user_id]
describe = "禁止回复和读取消息的QQ号列表"
important = false
can_edit = true
[personality.personality_core]
describe = "用一句话或几句话描述人格的核心特点建议20字以内"
important = true
can_edit = true
[personality.personality_sides]
describe = "用一句话或几句话描述人格的一些细节条数任意不能为0该选项还在调试中"
important = false
can_edit = true
[identity.identity_detail]
describe = "身份特点列表条数任意不能为0该选项还在调试中"
important = false
can_edit = true
[identity.age]
describe = "年龄,单位岁"
important = false
can_edit = true
[identity.gender]
describe = "性别"
important = false
can_edit = true
[identity.appearance]
describe = "外貌特征描述,该选项还在调试中,暂时未生效"
important = false
can_edit = true
[schedule.enable_schedule_gen]
describe = "是否启用日程表"
important = false
can_edit = true
[schedule.enable_schedule_interaction]
describe = "日程表是否影响回复模式"
important = false
can_edit = true
[schedule.prompt_schedule_gen]
describe = "用几句话描述描述性格特点或行动规律,这个特征会用来生成日程表"
important = false
can_edit = true
[schedule.schedule_doing_update_interval]
describe = "日程表更新间隔,单位秒"
important = false
can_edit = true
[schedule.schedule_temperature]
describe = "日程表温度建议0.1-0.5"
important = false
can_edit = true
[schedule.time_zone]
describe = "时区设置,可以解决运行电脑时区和国内时区不同的情况,或者模拟国外留学生日程"
important = false
can_edit = true
[platforms.nonebot-qq]
describe = "nonebot-qq适配器提供的链接"
important = true
can_edit = true
[chat.allow_focus_mode]
describe = "是否允许专注聊天状态"
important = false
can_edit = true
[chat.base_normal_chat_num]
describe = "最多允许多少个群进行普通聊天"
important = false
can_edit = true
[chat.base_focused_chat_num]
describe = "最多允许多少个群进行专注聊天"
important = false
can_edit = true
[chat.observation_context_size]
describe = "观察到的最长上下文大小建议15太短太长都会导致脑袋尖尖"
important = false
can_edit = true
[chat.message_buffer]
describe = "启用消息缓冲器,启用此项以解决消息的拆分问题,但会使麦麦的回复延迟"
important = false
can_edit = true
[chat.ban_words]
describe = "需要过滤的消息列表"
important = false
can_edit = true
[chat.ban_msgs_regex]
describe = "需要过滤的消息原始消息匹配的正则表达式匹配到的消息将被过滤支持CQ码"
important = false
can_edit = true

View File

@ -1,5 +1,5 @@
[inner]
version = "1.6.1"
version = "1.6.2"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请在修改后将version的值进行变更
@ -54,26 +54,16 @@ age = 20 # 年龄 单位岁
gender = "男" # 性别
appearance = "用几句话描述外貌特征" # 外貌特征 该选项还在调试中,暂时未生效
[schedule]
enable_schedule_gen = true # 是否启用日程表
enable_schedule_interaction = true # 日程表是否影响回复模式
prompt_schedule_gen = "用几句话描述描述性格特点或行动规律,这个特征会用来生成日程表"
schedule_doing_update_interval = 900 # 日程表更新间隔 单位秒
schedule_temperature = 0.1 # 日程表温度建议0.1-0.5
time_zone = "Asia/Shanghai" # 给你的机器人设置时区,可以解决运行电脑时区和国内时区不同的情况,或者模拟国外留学生日程
[platforms] # 必填项目,填写每个平台适配器提供的链接
nonebot-qq="http://127.0.0.1:18002/api/message"
[chat] #麦麦的聊天通用设置
allow_focus_mode = true # 是否允许专注聊天状态
allow_focus_mode = false # 是否允许专注水群状态
# 是否启用heart_flowC(HFC)模式
# 启用后麦麦会自主选择进入heart_flowC模式持续一段时间进行主动的观察和回复并给出回复比较消耗token
base_normal_chat_num = 8 # 最多允许多少个群进行普通聊天
base_focused_chat_num = 5 # 最多允许多少个群进行专注聊天
observation_context_size = 15 # 观察到的最长上下文大小,建议15太短太长都会导致脑袋尖尖
message_buffer = true # 启用消息缓冲器?启用此项以解决消息的拆分问题,但会使麦麦的回复延迟
message_buffer = false # 启用消息缓冲器?启用此项以解决消息的拆分问题,但会使麦麦的回复延迟
# 以下是消息过滤,可以根据规则过滤特定消息,将不会读取这些消息
ban_words = [
@ -103,14 +93,10 @@ emoji_response_penalty = 0 # 表情包回复惩罚系数设为0为不回复
mentioned_bot_inevitable_reply = false # 提及 bot 必然回复
at_bot_inevitable_reply = false # @bot 必然回复
[focus_chat] #专注聊天
reply_trigger_threshold = 3.6 # 专注聊天触发阈值,越低越容易进入专注聊天
default_decay_rate_per_second = 0.95 # 默认衰减率,越大衰减越快,越高越难进入专注聊天
consecutive_no_reply_threshold = 3 # 连续不回复的阈值,越低越容易结束专注聊天
# 以下选项暂时无效
compressed_length = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度超过心流观察到的上下文长度会压缩最短压缩长度为5
compress_length_limit = 5 #最多压缩份数,超过该数值的压缩上下文会被删除
[focus_chat] #专注水群
reply_trigger_threshold = 4 # 专注水群触发阈值,越低越容易进入专注水群
default_decay_rate_per_second = 0.9 # 默认衰减率,越大衰减越快,越高越难进入专注水群
consecutive_no_reply_threshold = 3 # 连续不回复的阈值,越低越容易结束专注水群
[emoji]