diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
index 76636d746..605d838ce 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -24,6 +24,9 @@ jobs:
- name: Clone maim_message
run: git clone https://github.com/MaiM-with-u/maim_message maim_message
+ - name: Clone lpmm
+ run: git clone https://github.com/MaiM-with-u/MaiMBot-LPMM.git MaiMBot-LPMM
+
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
diff --git a/Dockerfile b/Dockerfile
index 074711523..23165a23e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,15 +6,22 @@ WORKDIR /MaiMBot
# 复制依赖列表
COPY requirements.txt .
-# 同级目录下需要有 maim_message
-COPY maim_message /maim_message
+# 同级目录下需要有 maim_message MaiMBot-LPMM
+#COPY maim_message /maim_message
+COPY MaiMBot-LPMM /MaiMBot-LPMM
# 编译器
-RUN apt-get update && apt-get install -y g++
+RUN apt-get update && apt-get install -y build-essential
+
+# lpmm编译安装
+RUN cd /MaiMBot-LPMM && uv pip install --system -r requirements.txt
+RUN uv pip install --system Cython py-cpuinfo setuptools
+RUN cd /MaiMBot-LPMM/lib/quick_algo && python build_lib.py --cleanup --cythonize --install
+
# 安装依赖
RUN uv pip install --system --upgrade pip
-RUN uv pip install --system -e /maim_message
+#RUN uv pip install --system -e /maim_message
RUN uv pip install --system -r requirements.txt
# 复制项目代码
diff --git a/README.md b/README.md
index 7eca22601..65e04a233 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
@@ -180,6 +180,10 @@ MaiCore是一个开源项目,我们非常欢迎你的参与。你的贡献,
- [NapCat](https://github.com/NapNeko/NapCatQQ): 现代化的基于 NTQQ 的 Bot 协议端实现
+## 麦麦仓库状态
+
+
+
### 贡献者
感谢各位大佬!
diff --git a/changelogs/changelog.md b/changelogs/changelog.md
index 0ddb486bf..4fed6fb17 100644
--- a/changelogs/changelog.md
+++ b/changelogs/changelog.md
@@ -1,5 +1,80 @@
# Changelog
+## [0.6.3] - 2025-4-15
+
+### 摘要
+- MaiBot 0.6.3 版本发布!核心重构回复逻辑,统一为心流系统管理,智能切换交互模式。
+- 引入全新的 LPMM 知识库系统,大幅提升信息获取能力。
+- 新增昵称系统,改善群聊中的身份识别。
+- 提供独立的桌宠适配器连接程序。
+- 优化日志输出,修复若干问题。
+
+### 🌟 核心功能增强
+#### 统一回复逻辑 (Unified Reply Logic)
+- **核心重构**: 移除了经典 (Reasoning) 与心流 (Heart Flow) 模式的区分,将回复逻辑完全整合到 `SubHeartflow` 中进行统一管理,由主心流统一调控。保留 Heart FC 模式的特色功能。
+- **智能交互模式**: `SubHeartflow` 现在可以根据情境智能选择不同的交互模式:
+ - **普通聊天 (Normal Chat)**: 类似于之前的 Reasoning 模式,进行常规回复(激活逻辑暂未改变)。
+ - **心流聊天 (Heart Flow Chat)**: 基于改进的 PFC 模式,能更好地理解上下文,减少重复和认错人的情况,并支持**工具调用**以获取额外信息。
+ - **离线模式 (Offline/Absent)**: 在特定情况下,麦麦可能会选择暂时不查看或回复群聊消息。
+- **状态管理**: 交互模式的切换由 `SubHeartflow` 内部逻辑和 `SubHeartflowManager` 根据整体状态 (`MaiState`) 和配置进行管理。
+- **流程优化**: 拆分了子心流的思考模块,使整体对话流程更加清晰。
+- **状态判断改进**: 将 CHAT 状态判断交给 LLM 处理,使对话更自然。
+- **回复机制**: 实现更为灵活的概率回复机制,使机器人能够自然地融入群聊环境。
+
+#### 全新知识库系统 (New Knowledge Base System - LPMM)
+- **引入 LPMM**: 新增了 **LPMM (Large Psychology Model Maker)** 知识库系统,具有强大的信息检索能力,能显著提升麦麦获取和利用知识的效率。
+- **功能集成**: 集成了 LPMM 知识库查询功能,进一步扩展信息检索能力。
+- **推荐使用**: 强烈建议使用新的 LPMM 系统以获得最佳体验。旧的知识库系统仍然可用作为备选。
+
+#### 昵称系统 (Nickname System)
+- **自动取名**: 麦麦现在会尝试给群友取昵称,减少对易变的群昵称的依赖,从而降低认错人的概率。
+- **持续完善**: 该系统目前仍处于早期阶段,会持续进行优化。
+
+#### 记忆与上下文增强 (Memory and Context Enhancement)
+- **聊天记录压缩**: 大幅优化聊天记录压缩系统,使机器人能够处理5倍于之前的上下文记忆量。
+- **记忆提取**: 优化记忆提取功能,提高对历史对话的理解和引用能力。
+- **中期记忆调用**: 完善中期记忆调用机制,使机器人能够更自然地回忆和引用较早前的对话。
+
+#### 私聊 PFC 功能增强 (Private Chat PFC Enhancement)
+- **功能修复与优化**: 修复了私聊 PFC 载入聊天记录缺失的 bug,优化了 prompt 构建,增加了审核机制,调整了重试次数,并将机器人发言存入数据库。
+- **实验性质**: 请注意,PFC 仍然是一个实验性功能,可能在未来版本中被修改或移除,目前不接受相关 Bug 反馈。
+
+#### 情感与互动增强 (Emotion and Interaction Enhancement)
+- **全新表情包系统**: 新的表情包系统上线,表情含义更丰富,发送更快速。
+- **提示词优化**: 优化提示词(prompt)构建,增强对话质量和情感表达。
+- **积极性配置**: 优化"让麦麦更愿意说话"的相关配置,使机器人更积极参与对话。
+- **命名统一**: 实现统一命名功能,自动替换 prompt 内唯一标识符,优化 prompt 效果。
+- **颜文字保护**: 保护颜文字处理机制,确保表情正确显示。
+
+#### 工具与集成 (Tools and Integration)
+- **动态更新**: 使用工具调用来更新关系和心情,取代原先的固定更新机制。
+- **智能调用**: 工具调用时会考虑上下文,使调用更加智能。
+- **知识库依赖**: 添加 LPMM 知识库依赖,扩展知识检索工具。
+
+### 💻 系统架构优化
+#### 日志优化 (Logging Optimization)
+- **输出更清晰**: 优化了日志信息的格式和内容,使其更易于阅读和理解。
+
+#### 模型与消息整合 (Model and Message Integration)
+- **模型合并**: 合并工具调用模型和心流模型,提高整体一致性。
+- **消息规范**: 全面改用 `maim_message`,移除对 `rest` 的支持。
+
+#### (临时) 简易 GUI (Temporary Simple GUI)
+- **运行状态查看**: 提供了一个非常基础的图形用户界面,用于查看麦麦的运行状态。
+- **临时方案**: 这是一个临时性的解决方案,功能简陋,**将在 0.6.4 版本中被全新的 Web UI 所取代**。此 GUI 不会包含在主程序包中,而是通过一键包提供,并且不接受 Bug 反馈。
+
+### 🐛 问题修复
+- **记忆检索优化**: 提高了记忆检索的准确性和效率。
+- 修复了一些其他小问题。
+
+### 🔧 其他改进
+#### 桌宠适配器 (Bug Catcher Adapter)
+- **独立适配器**: 提供了一个"桌宠"独立适配器,用于连接麦麦和桌宠。
+- **获取方式**: 可在 MaiBot 的 GitHub 组织中找到该适配器,不包含在主程序内。
+
+#### 一键包内容 (One-Click Package Contents)
+- **辅助程序**: 一键包中包含了简易 GUI 和 **麦麦帮助配置** 等辅助程序,后者可在配置出现问题时提供帮助。
+
## [0.6.2] - 2025-4-14
### 摘要
diff --git a/interest_monitor_gui.py b/interest_monitor_gui.py
index 28c5ecc14..fb9e51cf8 100644
--- a/interest_monitor_gui.py
+++ b/interest_monitor_gui.py
@@ -246,7 +246,9 @@ class InterestMonitorApp:
self.stream_sub_minds[stream_id] = subflow_entry.get("sub_mind", "N/A")
self.stream_chat_states[stream_id] = subflow_entry.get("sub_chat_state", "N/A")
self.stream_threshold_status[stream_id] = subflow_entry.get("is_above_threshold", False)
- self.stream_last_active[stream_id] = subflow_entry.get("last_active_time") # 存储原始时间戳
+ self.stream_last_active[stream_id] = subflow_entry.get(
+ "chat_state_changed_time"
+ ) # 存储原始时间戳
self.stream_last_interaction[stream_id] = subflow_entry.get(
"last_interaction_time"
) # 存储原始时间戳
diff --git a/llm_tool_benchmark_results.json b/llm_tool_benchmark_results.json
deleted file mode 100644
index e6be2a7dc..000000000
--- a/llm_tool_benchmark_results.json
+++ /dev/null
@@ -1,71 +0,0 @@
-{
- "测试时间": "2025-04-24 13:22:36",
- "测试迭代次数": 3,
- "不使用工具调用": {
- "平均耗时": 3.1020479996999106,
- "最短耗时": 2.980656862258911,
- "最长耗时": 3.2487313747406006,
- "标准差": 0.13581516492157006,
- "所有耗时": [
- 2.98,
- 3.08,
- 3.25
- ]
- },
- "不使用工具调用_详细响应": [
- {
- "内容摘要": "那个猫猫头表情包真的太可爱了,墨墨发的表情包也好萌,感觉可以分享一下我收藏的猫猫头系列",
- "推理内容摘要": ""
- },
- {
- "内容摘要": "那个猫猫头表情包确实很魔性,我存了好多张,每次看到都觉得特别治愈。墨墨好像也喜欢这种可爱的表情包,可以分享一下我收藏的。",
- "推理内容摘要": ""
- },
- {
- "内容摘要": "那个猫猫头表情包真的超可爱,我存了好多张,每次看到都会忍不住笑出来。墨墨发的表情包也好萌,感觉可以和大家分享一下我收藏的猫猫头。\n\n工具:无",
- "推理内容摘要": ""
- }
- ],
- "使用工具调用": {
- "平均耗时": 7.927528937657674,
- "最短耗时": 5.714647531509399,
- "最长耗时": 11.046205997467041,
- "标准差": 2.778799784731646,
- "所有耗时": [
- 7.02,
- 11.05,
- 5.71
- ]
- },
- "使用工具调用_详细响应": [
- {
- "内容摘要": "这个猫猫头表情包确实挺有意思的,不过他们好像还在讨论版本问题。小千石在问3.8和3.11谁大,这挺简单的。",
- "推理内容摘要": "",
- "工具调用数量": 1,
- "工具调用详情": [
- {
- "工具名称": "compare_numbers",
- "参数": "{\"num1\":3.8,\"num2\":3.11}"
- }
- ]
- },
- {
- "内容摘要": "3.8和3.11谁大这个问题有点突然,不过可以简单比较一下。可能小千石在测试我或者真的想知道答案。现在群里的话题有点分散,既有技术讨论又有表情包的话题,我还是先回答数字比较的问题好了,毕竟比较直接。",
- "推理内容摘要": "",
- "工具调用数量": 1,
- "工具调用详情": [
- {
- "工具名称": "compare_numbers",
- "参数": "{\"num1\":3.8,\"num2\":3.11}"
- }
- ]
- },
- {
- "内容摘要": "他们还在纠结调试消息的事儿,不过好像讨论得差不多了。猫猫头表情包确实挺有意思的,但感觉聊得有点散了哦。小千石问3.8和3.11谁大,这个问题可以回答一下。",
- "推理内容摘要": "",
- "工具调用数量": 0,
- "工具调用详情": []
- }
- ],
- "差异百分比": 155.56
-}
\ No newline at end of file
diff --git a/scripts/run.sh b/scripts/run.sh
index b7ecbc849..9fd3127f6 100644
--- a/scripts/run.sh
+++ b/scripts/run.sh
@@ -4,7 +4,7 @@
# 适用于Arch/Ubuntu 24.10/Debian 12/CentOS 9
# 请小心使用任何一键脚本!
-INSTALLER_VERSION="0.0.3-refactor"
+INSTALLER_VERSION="0.0.4-refactor"
LANG=C.UTF-8
# 如无法访问GitHub请修改此处镜像地址
@@ -19,10 +19,10 @@ RESET="\e[0m"
declare -A REQUIRED_PACKAGES=(
["common"]="git sudo python3 curl gnupg"
- ["debian"]="python3-venv python3-pip"
- ["ubuntu"]="python3-venv python3-pip"
- ["centos"]="python3-pip"
- ["arch"]="python-virtualenv python-pip"
+ ["debian"]="python3-venv python3-pip build-essential"
+ ["ubuntu"]="python3-venv python3-pip build-essential"
+ ["centos"]="epel-release python3-pip python3-devel gcc gcc-c++ make"
+ ["arch"]="python-virtualenv python-pip base-devel"
)
# 默认项目目录
diff --git a/src/common/logger.py b/src/common/logger.py
index 8a5b7ffc7..176d4629c 100644
--- a/src/common/logger.py
+++ b/src/common/logger.py
@@ -5,7 +5,57 @@ import os
from types import ModuleType
from pathlib import Path
from dotenv import load_dotenv
-# from ..plugins.chat.config import global_config
+
+"""
+日志颜色说明:
+
+1. 主程序(Main)
+浅黄色标题 | 浅黄色消息
+
+2. 海马体(Memory)
+浅黄色标题 | 浅黄色消息
+
+3. PFC(前额叶皮质)
+浅绿色标题 | 浅绿色消息
+
+4. 心情(Mood)
+品红色标题 | 品红色消息
+
+5. 工具使用(Tool)
+品红色标题 | 品红色消息
+
+6. 关系(Relation)
+浅品红色标题 | 浅品红色消息
+
+7. 配置(Config)
+浅青色标题 | 浅青色消息
+
+8. 麦麦大脑袋
+浅绿色标题 | 浅绿色消息
+
+9. 在干嘛
+青色标题 | 青色消息
+
+10. 麦麦组织语言
+浅绿色标题 | 浅绿色消息
+
+11. 见闻(Chat)
+浅蓝色标题 | 绿色消息
+
+12. 表情包(Emoji)
+橙色标题 | 橙色消息 fg #FFD700
+
+13. 子心流
+
+13. 其他模块
+模块名标题 | 对应颜色消息
+
+
+注意:
+1. 级别颜色遵循loguru默认配置
+2. 可通过环境变量修改日志级别
+"""
+
# 加载 .env 文件
env_path = Path(__file__).resolve().parent.parent.parent / ".env"
@@ -88,25 +138,6 @@ MAIN_STYLE_CONFIG = {
},
}
-# 海马体日志样式配置
-MEMORY_STYLE_CONFIG = {
- "advanced": {
- "console_format": (
- "{time:YYYY-MM-DD HH:mm:ss} | "
- "{level: <8} | "
- "海马体 | "
- "{message}"
- ),
- "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}",
- },
- "simple": {
- "console_format": (
- "{time:MM-DD HH:mm} | 海马体 | {message}"
- ),
- "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}",
- },
-}
-
# pfc配置
PFC_STYLE_CONFIG = {
"advanced": {
@@ -132,13 +163,13 @@ MOOD_STYLE_CONFIG = {
"console_format": (
"{time:YYYY-MM-DD HH:mm:ss} | "
"{level: <8} | "
- "心情 | "
+ "心情 | "
"{message}"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}",
},
"simple": {
- "console_format": "{time:MM-DD HH:mm} | 心情 | {message}",
+ "console_format": "{time:MM-DD HH:mm} | 心情 | {message} ",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}",
},
}
@@ -284,15 +315,13 @@ CHAT_STYLE_CONFIG = {
"console_format": (
"{time:YYYY-MM-DD HH:mm:ss} | "
"{level: <8} | "
- "见闻 | "
+ "见闻 | "
"{message}"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}",
},
"simple": {
- "console_format": (
- "{time:MM-DD HH:mm} | 见闻 | {message}"
- ), # noqa: E501
+ "console_format": ("{time:MM-DD HH:mm} | 见闻 | {message}"), # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}",
},
}
@@ -308,12 +337,28 @@ REMOTE_STYLE_CONFIG = {
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 远程 | {message}",
},
"simple": {
- "console_format": "{time:MM-DD HH:mm} | 远程 | {message}",
+ "console_format": "{time:MM-DD HH:mm} | 远程| {message}",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 远程 | {message}",
},
}
SUB_HEARTFLOW_STYLE_CONFIG = {
+ "advanced": {
+ "console_format": (
+ "{time:YYYY-MM-DD HH:mm:ss} | "
+ "{level: <8} | "
+ "麦麦水群 | "
+ "{message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
+ },
+ "simple": {
+ "console_format": ("{time:MM-DD HH:mm} | 麦麦水群 | {message}"), # noqa: E501
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群 | {message}",
+ },
+}
+
+SUB_HEARTFLOW_MIND_STYLE_CONFIG = {
"advanced": {
"console_format": (
"{time:YYYY-MM-DD HH:mm:ss} | "
@@ -324,13 +369,27 @@ SUB_HEARTFLOW_STYLE_CONFIG = {
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
},
"simple": {
- "console_format": (
- "{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}"
- ), # noqa: E501
+ "console_format": ("{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}"), # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}",
},
}
+SUBHEARTFLOW_MANAGER_STYLE_CONFIG = {
+ "advanced": {
+ "console_format": (
+ "{time:YYYY-MM-DD HH:mm:ss} | "
+ "{level: <8} | "
+ "麦麦水群[管理] | "
+ "{message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}",
+ },
+ "simple": {
+ "console_format": ("{time:MM-DD HH:mm} | 麦麦水群[管理] | {message}"), # noqa: E501
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}",
+ },
+}
+
BASE_TOOL_STYLE_CONFIG = {
"advanced": {
"console_format": (
@@ -403,24 +462,6 @@ BACKGROUND_TASKS_STYLE_CONFIG = {
},
}
-SUBHEARTFLOW_MANAGER_STYLE_CONFIG = {
- "advanced": {
- "console_format": (
- "{time:YYYY-MM-DD HH:mm:ss} | "
- "{level: <8} | "
- "小脑袋管理 | "
- "{message}"
- ),
- "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 小脑袋管理 | {message}",
- },
- "simple": {
- "console_format": (
- "{time:MM-DD HH:mm} | 小脑袋管理 | {message}"
- ), # noqa: E501
- "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 小脑袋管理 | {message}",
- },
-}
-
WILLING_STYLE_CONFIG = {
"advanced": {
"console_format": (
@@ -453,19 +494,20 @@ PFC_ACTION_PLANNER_STYLE_CONFIG = {
},
}
+# EMOJI,橙色,全着色
EMOJI_STYLE_CONFIG = {
"advanced": {
"console_format": (
"{time:YYYY-MM-DD HH:mm:ss} | "
"{level: <8} | "
- "表情 | "
+ "表情包 | "
"{message}"
),
- "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": "{time:MM-DD HH:mm} | 表情 | {message} ", # noqa: E501
- "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 表情 | {message}",
+ "console_format": "{time:MM-DD HH:mm} | 表情包 | {message} ", # noqa: E501
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 表情包 | {message}",
},
}
@@ -480,11 +522,32 @@ MAI_STATE_CONFIG = {
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦状态 | {message}",
},
"simple": {
- "console_format": "{time:MM-DD HH:mm} | 麦麦状态 | {message} ", # noqa: E501
+ "console_format": "{time:MM-DD HH:mm} | 麦麦状态 | {message} ", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦状态 | {message}",
},
}
+
+# 海马体日志样式配置
+MEMORY_STYLE_CONFIG = {
+ "advanced": {
+ "console_format": (
+ "{time:YYYY-MM-DD HH:mm:ss} | "
+ "{level: <8} | "
+ "海马体 | "
+ "{message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}",
+ },
+ "simple": {
+ "console_format": (
+ "{time:MM-DD HH:mm} | 海马体 | {message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}",
+ },
+}
+
+
# LPMM配置
LPMM_STYLE_CONFIG = {
"advanced": {
@@ -498,7 +561,7 @@ LPMM_STYLE_CONFIG = {
},
"simple": {
"console_format": (
- "{time:MM-DD HH:mm} | LPMM | {message}"
+ "{time:MM-DD HH:mm} | LPMM | {message}"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | LPMM | {message}",
},
@@ -528,6 +591,25 @@ CONFIRM_STYLE_CONFIG = {
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}",
}
+# 天依蓝配置
+TIANYI_STYLE_CONFIG = {
+ "advanced": {
+ "console_format": (
+ "{time:YYYY-MM-DD HH:mm:ss} | "
+ "{level: <8} | "
+ "天依 | "
+ "{message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 天依 | {message}",
+ },
+ "simple": {
+ "console_format": (
+ "{time:MM-DD HH:mm} | 天依 | {message}"
+ ),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 天依 | {message}",
+ },
+}
+
# 根据SIMPLE_OUTPUT选择配置
MAIN_STYLE_CONFIG = MAIN_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MAIN_STYLE_CONFIG["advanced"]
EMOJI_STYLE_CONFIG = EMOJI_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else EMOJI_STYLE_CONFIG["advanced"]
@@ -556,6 +638,9 @@ HEARTFLOW_STYLE_CONFIG = HEARTFLOW_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else
SUB_HEARTFLOW_STYLE_CONFIG = (
SUB_HEARTFLOW_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SUB_HEARTFLOW_STYLE_CONFIG["advanced"]
) # noqa: E501
+SUB_HEARTFLOW_MIND_STYLE_CONFIG = (
+ SUB_HEARTFLOW_MIND_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SUB_HEARTFLOW_MIND_STYLE_CONFIG["advanced"]
+)
WILLING_STYLE_CONFIG = WILLING_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else WILLING_STYLE_CONFIG["advanced"]
MAI_STATE_CONFIG = MAI_STATE_CONFIG["simple"] if SIMPLE_OUTPUT else MAI_STATE_CONFIG["advanced"]
CONFIG_STYLE_CONFIG = CONFIG_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CONFIG_STYLE_CONFIG["advanced"]
@@ -563,6 +648,7 @@ TOOL_USE_STYLE_CONFIG = TOOL_USE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TO
PFC_STYLE_CONFIG = PFC_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PFC_STYLE_CONFIG["advanced"]
LPMM_STYLE_CONFIG = LPMM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else LPMM_STYLE_CONFIG["advanced"]
INTEREST_STYLE_CONFIG = INTEREST_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else INTEREST_STYLE_CONFIG["advanced"]
+TIANYI_STYLE_CONFIG = TIANYI_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TIANYI_STYLE_CONFIG["advanced"]
def is_registered_module(record: dict) -> bool:
diff --git a/src/config/config.py b/src/config/config.py
index 7c16aaa59..2ade83f11 100644
--- a/src/config/config.py
+++ b/src/config/config.py
@@ -28,7 +28,7 @@ logger = get_module_logger("config", config=config_config)
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
is_test = True
mai_version_main = "0.6.3"
-mai_version_fix = "snapshot-4"
+mai_version_fix = "snapshot-5"
if mai_version_fix:
if is_test:
@@ -192,7 +192,6 @@ class BotConfig:
reply_trigger_threshold: float = 3.0 # 心流聊天触发阈值,越低越容易触发
probability_decay_factor_per_second: float = 0.2 # 概率衰减因子,越大衰减越快
default_decay_rate_per_second: float = 0.98 # 默认衰减率,越大衰减越慢
- initial_duration: int = 60 # 初始持续时间,越大心流聊天持续的时间越长
# sub_heart_flow_update_interval: int = 60 # 子心流更新频率,间隔 单位秒
# sub_heart_flow_freeze_time: int = 120 # 子心流冻结时间,超过这个时间没有回复,子心流会冻结,间隔 单位秒
@@ -221,8 +220,11 @@ class BotConfig:
max_emoji_num: int = 200 # 表情包最大数量
max_reach_deletion: bool = True # 开启则在达到最大数量时删除表情包,关闭则不会继续收集表情包
EMOJI_CHECK_INTERVAL: int = 120 # 表情包检查间隔(分钟)
- EMOJI_REGISTER_INTERVAL: int = 10 # 表情包注册间隔(分钟)
- EMOJI_SAVE: bool = True # 偷表情包
+
+ save_pic: bool = False # 是否保存图片
+ save_emoji: bool = False # 是否保存表情包
+ steal_emoji: bool = True # 是否偷取表情包,让麦麦可以发送她保存的这些表情包
+
EMOJI_CHECK: bool = False # 是否开启过滤
EMOJI_CHECK_PROMPT: str = "符合公序良俗" # 表情包过滤要求
@@ -259,6 +261,7 @@ class BotConfig:
chinese_typo_word_replace_rate = 0.02 # 整词替换概率
# response_splitter
+ enable_kaomoji_protection = False # 是否启用颜文字保护
enable_response_splitter = True # 是否启用回复分割器
response_max_length = 100 # 回复允许的最大长度
response_max_sentence_num = 3 # 回复允许的最大句子数
@@ -282,11 +285,11 @@ class BotConfig:
vlm: Dict[str, str] = field(default_factory=lambda: {})
moderation: Dict[str, str] = field(default_factory=lambda: {})
- # 实验性
llm_observation: Dict[str, str] = field(default_factory=lambda: {})
llm_sub_heartflow: Dict[str, str] = field(default_factory=lambda: {})
llm_heartflow: Dict[str, str] = field(default_factory=lambda: {})
llm_tool_use: Dict[str, str] = field(default_factory=lambda: {})
+ llm_plan: Dict[str, str] = field(default_factory=lambda: {})
api_urls: Dict[str, str] = field(default_factory=lambda: {})
@@ -390,13 +393,15 @@ class BotConfig:
def emoji(parent: dict):
emoji_config = parent["emoji"]
config.EMOJI_CHECK_INTERVAL = emoji_config.get("check_interval", config.EMOJI_CHECK_INTERVAL)
- config.EMOJI_REGISTER_INTERVAL = emoji_config.get("register_interval", config.EMOJI_REGISTER_INTERVAL)
config.EMOJI_CHECK_PROMPT = emoji_config.get("check_prompt", config.EMOJI_CHECK_PROMPT)
- config.EMOJI_SAVE = emoji_config.get("auto_save", config.EMOJI_SAVE)
config.EMOJI_CHECK = emoji_config.get("enable_check", config.EMOJI_CHECK)
if config.INNER_VERSION in SpecifierSet(">=1.1.1"):
config.max_emoji_num = emoji_config.get("max_emoji_num", config.max_emoji_num)
config.max_reach_deletion = emoji_config.get("max_reach_deletion", config.max_reach_deletion)
+ if config.INNER_VERSION in SpecifierSet(">=1.4.2"):
+ config.save_pic = emoji_config.get("save_pic", config.save_pic)
+ config.save_emoji = emoji_config.get("save_emoji", config.save_emoji)
+ config.steal_emoji = emoji_config.get("steal_emoji", config.steal_emoji)
def bot(parent: dict):
# 机器人基础配置
@@ -421,21 +426,9 @@ class BotConfig:
def heartflow(parent: dict):
heartflow_config = parent["heartflow"]
- # 加载新增的 heartflowC 参数
-
- # 加载原有的 heartflow 参数
- # config.sub_heart_flow_update_interval = heartflow_config.get(
- # "sub_heart_flow_update_interval", config.sub_heart_flow_update_interval
- # )
- # config.sub_heart_flow_freeze_time = heartflow_config.get(
- # "sub_heart_flow_freeze_time", config.sub_heart_flow_freeze_time
- # )
config.sub_heart_flow_stop_time = heartflow_config.get(
"sub_heart_flow_stop_time", config.sub_heart_flow_stop_time
)
- # config.heart_flow_update_interval = heartflow_config.get(
- # "heart_flow_update_interval", config.heart_flow_update_interval
- # )
if config.INNER_VERSION in SpecifierSet(">=1.3.0"):
config.observation_context_size = heartflow_config.get(
"observation_context_size", config.observation_context_size
@@ -454,7 +447,6 @@ class BotConfig:
config.default_decay_rate_per_second = heartflow_config.get(
"default_decay_rate_per_second", config.default_decay_rate_per_second
)
- config.initial_duration = heartflow_config.get("initial_duration", config.initial_duration)
def willing(parent: dict):
willing_config = parent["willing"]
@@ -495,6 +487,7 @@ class BotConfig:
"llm_tool_use",
"llm_observation",
"llm_sub_heartflow",
+ "llm_plan",
"llm_heartflow",
"llm_PFC_action_planner",
"llm_PFC_chat",
@@ -647,6 +640,10 @@ class BotConfig:
config.response_max_sentence_num = response_splitter_config.get(
"response_max_sentence_num", config.response_max_sentence_num
)
+ if config.INNER_VERSION in SpecifierSet(">=1.4.2"):
+ config.enable_kaomoji_protection = response_splitter_config.get(
+ "enable_kaomoji_protection", config.enable_kaomoji_protection
+ )
def groups(parent: dict):
groups_config = parent["groups"]
diff --git a/src/heart_flow/0.6.3TODO.md b/src/heart_flow/0.6.3TODO.md
new file mode 100644
index 000000000..4048fec8f
--- /dev/null
+++ b/src/heart_flow/0.6.3TODO.md
@@ -0,0 +1,48 @@
+# 0.6.3 版本发布前待办事项
+
+- **统一化人格配置:**
+ - 检查代码中是否存在硬编码的人格相关配置。
+ - 将所有硬编码的人格配置替换为使用 `individual` 模块进行管理。
+
+- **在 Planner 中添加回复计数信息:**
+ - 修改 `HeartFlowChatInstance` 的 `Plan` 阶段逻辑。
+ - 将当前周期的回复计数(或其他相关统计信息)作为输入提供给 Planner。
+ - 目的是为 Planner 提供负反馈,减少连续回复或不当回复的可能性。
+
+- **恢复/检查被停止的功能:**
+ - 全面审查代码,特别是对比之前的版本或设计文档。
+ - 识别并重新启用那些暂时被禁用但应该恢复的功能。
+ - 确认没有核心功能意外丢失。
+
+- **参数提取与配置化:**
+ - 识别代码中散落的各种可调参数(例如:概率阈值、时间间隔、次数限制、LLM 模型名称等)。
+ - 将这些参数统一提取到模块或类的顶部。
+ - 最终将这些参数移至外部配置文件(如 YAML 或 JSON 文件),方便用户自定义。
+
+- **提供 HFC (HeartFlowChatInstance) 开启/关闭选项:**
+ - 增加一个全局或针对特定子心流的配置选项。
+ - 允许用户控制是否启用 `FOCUSED` 状态以及关联的 `HeartFlowChatInstance`。
+ - 如果禁用 HFC,子心流可能只会在 `ABSENT` 和 `CHAT` 状态间切换。
+
+- **添加防破线机制 (针对接收消息):**
+ - 在消息处理流程的早期阶段 (例如 `HeartHC_processor` 或类似模块),增加对接收到的消息文本长度的检查。
+ - 对超过预设长度阈值的*接收*消息进行截断处理。
+ - 目的是防止过长的输入(可能包含"破限"提示词)影响后续的兴趣计算、LLM 回复生成等环节。
+
+- **NormalChat 模式下的记忆与 Prompt 优化:**
+ - 重点审视 `NormalChatInstance` (闲聊/推理模式) 中记忆调用 (例如 `HippocampusManager` 的使用) 的方式。
+ - 评估在该模式下引入工具调用 (Tool Calling) 机制以更结构化访问记忆的必要性。
+ - 优化 `NormalChatInstance` 中与记忆检索、应用相关的 Prompt。
+
+- **完善简易兴趣监控 GUI:**
+ - 改进现有的、用于监控聊天兴趣度 (`InterestChatting`?) 的简单 GUI 界面。
+ - 使其能更清晰地展示关键参数和状态,作为查看日志之外的更直观的监控方式。
+ - 作为完整外部 UI 开发完成前的临时替代方案。
+
+- **修复/完善中期记忆 (Midterm Memory):**
+ - 检查当前中期记忆模块的状态。
+ - 修复已知问题,使其能够稳定运行。
+ - (优先级视开发时间而定)
+
+
+对于有些群频繁激活HFC,却不回复,需要处理一下
diff --git a/src/heart_flow/0.6Bing.md b/src/heart_flow/0.6Bing.md
new file mode 100644
index 000000000..6af057e29
--- /dev/null
+++ b/src/heart_flow/0.6Bing.md
@@ -0,0 +1,84 @@
+- **智能化 MaiState 状态转换**:
+ - 当前 `MaiState` (整体状态,如 `OFFLINE`, `NORMAL_CHAT` 等) 的转换逻辑 (`MaiStateManager`) 较为简单,主要依赖时间和随机性。
+ - 未来的计划是让主心流 (`Heartflow`) 负责决策自身的 `MaiState`。
+ - 该决策将综合考虑以下信息:
+ - 各个子心流 (`SubHeartflow`) 的活动状态和信息摘要。
+ - 主心流自身的状态和历史信息。
+ - (可能) 结合预设的日程安排 (Schedule) 信息。
+ - 目标是让 Mai 的整体状态变化更符合逻辑和上下文。 (计划在 064 实现)
+
+- **参数化与动态调整聊天行为**:
+ - 将 `NormalChatInstance` 和 `HeartFlowChatInstance` 中的关键行为参数(例如:回复概率、思考频率、兴趣度阈值、状态转换条件等)提取出来,使其更易于配置。
+ - 允许每个 `SubHeartflow` (即每个聊天场景) 拥有其独立的参数配置,实现"千群千面"。
+ - 开发机制,使得这些参数能够被动态调整:
+ - 基于外部反馈:例如,根据用户评价("话太多"或"太冷淡")调整回复频率。
+ - 基于环境分析:例如,根据群消息的活跃度自动调整参与度。
+ - 基于学习:通过分析历史交互数据,优化特定群聊下的行为模式。
+ - 目标是让 Mai 在不同群聊中展现出更适应环境、更个性化的交互风格。
+
+- **动态 Prompt 生成与人格塑造**:
+ - 当前 Prompt (提示词) 相对静态。计划实现动态或半结构化的 Prompt 生成。
+ - Prompt 内容可根据以下因素调整:
+ - **人格特质**: 通过参数化配置(如友善度、严谨性等),影响 Prompt 的措辞、语气和思考倾向,塑造更稳定和独特的人格。
+ - **当前情绪**: 将实时情绪状态融入 Prompt,使回复更符合当下心境。
+ - 目标:提升 `HeartFlowChatInstance` (HFC) 回复的多样性、一致性和真实感。
+ - 前置:需要重构 Prompt 构建逻辑,可能引入 `PromptBuilder` 并提供标准接口 (认为是必须步骤)。
+
+- **扩展观察系统 (Observation System)**:
+ - 目前主要依赖 `ChattingObservation` 获取消息。
+ - 计划引入更多 `Observation` 类型,为 `SubHeartflow` 提供更丰富的上下文:
+ - Mai 的全局状态 (`MaiStateInfo`)。
+ - `SubHeartflow` 自身的聊天状态 (`ChatStateInfo`) 和参数配置。
+ - Mai 的系统配置、连接平台信息。
+ - 其他相关聊天或系统的聚合信息。
+ - 目标:让 `SubHeartflow` 基于更全面的信息进行决策。
+
+- **增强工具调用能力 (Enhanced Tool Usage)**:
+ - 扩展 `HeartFlowChatInstance` (HFC) 可用的工具集。
+ - 考虑引入"元工具"或分层工具机制,允许 HFC 在需要时(如深度思考)访问更强大的工具,例如:
+ - 修改自身或其他 `SubHeartflow` 的聊天参数。
+ - 请求改变 Mai 的全局状态 (`MaiState`)。
+ - 管理日程或执行更复杂的分析任务。
+ - 目标:提升 HFC 的自主决策和行动能力,即使会增加一定的延迟。
+
+- **基于历史学习的行为模式应用**:
+ - **学习**: 分析过往聊天记录,提取和学习具体的行为模式(如特定梗的用法、情境化回应风格等)。可能需要专门的分析模块。
+ - **存储与匹配**: 需要有效的方法存储学习到的行为模式,并开发强大的 **匹配** 机制,在运行时根据当前情境检索最合适的模式。**(匹配的准确性是关键)**
+ - **应用与评估**: 将匹配到的行为模式融入 HFC 的决策和回复生成(例如,将其整合进 Prompt)。之后需评估该行为模式应用的实际效果。
+ - **人格塑造**: 通过学习到的实际行为来动态塑造人格,作为静态人设描述的补充或替代,使其更生动自然。
+
+- **标准化人设生成 (Standardized Persona Generation)**:
+ - **目标**: 解决手动配置 `人设` 文件缺乏标准、难以全面描述个性的问题,并生成更丰富、可操作的人格资源。
+ - **方法**: 利用大型语言模型 (LLM) 辅助生成标准化的、结构化的人格**资源包**。
+ - **生成内容**: 不仅生成描述性文本(替代现有 `individual` 配置),还可以同时生成与该人格配套的:
+ - **相关工具 (Tools)**: 该人格倾向于使用的工具或能力。
+ - **初始记忆/知识库 (Memories/Knowledge)**: 定义其背景和知识基础。
+ - **核心行为模式 (Core Behavior Patterns)**: 预置一些典型的行为方式,可作为行为学习的起点。
+ - **实现途径**:
+ - 通过与 LLM 的交互式对话来定义和细化人格及其配套资源。
+ - 让 LLM 分析提供的文本材料(如小说、背景故事)来提取人格特质和相关信息。
+ - **优势**: 替代易出错且标准不一的手动配置,生成更丰富、一致、包含配套资源且易于系统理解和应用的人格包。
+
+- **优化表情包处理与理解 (Enhanced Emoji Handling and Understanding)**:
+ - **面临挑战**:
+ - **历史记录表示**: 如何在聊天历史中有效表示表情包,供 LLM 理解。
+ - **语义理解**: 如何让 LLM 准确把握表情包的含义、情感和语境。
+ - **场景判断与选择**: 如何让 LLM 判断何时适合使用表情包,并选择最贴切的一个。
+ - **目标**: 提升 Mai 理解和运用表情包的能力,使交互更自然生动。
+ - **说明**: 可能需要较多时间进行数据处理和模型调优,但对改善体验潜力巨大。
+
+- **探索高级记忆检索机制 (GE 系统概念):**
+ - 研究超越简单关键词/近期性检索的记忆模型。
+ - 考虑引入基于事件关联、相对时间线索和绝对时间锚点的检索方式。
+ - 可能涉及设计新的事件表示或记忆结构。
+
+
+- **实现 SubHeartflow 级记忆缓存池:**
+ - 在 `SubHeartflow` 层级或更高层级设计并实现一个缓存池,存储已检索的记忆/信息。
+ - 避免在 HFC 等循环中重复进行相同的记忆检索调用。
+ - 确保存储的信息能有效服务于当前交互上下文。
+
+- **基于人格生成预设知识:**
+ - 开发利用 LLM 和人格配置生成背景知识的功能。
+ - 这些知识应符合角色的行为风格和可能的经历。
+ - 作为一种"冷启动"或丰富角色深度的方式。
\ No newline at end of file
diff --git a/src/heart_flow/README.md b/src/heart_flow/README.md
index dc00a9ff9..fdb3a1928 100644
--- a/src/heart_flow/README.md
+++ b/src/heart_flow/README.md
@@ -1,157 +1,241 @@
# 心流系统 (Heart Flow System)
-## 系统架构
+## 一条消息是怎么到最终回复的?简明易懂的介绍
-### 1. 主心流 (Heartflow)
-- 位于 `heartflow.py`
-- 作为整个系统的主控制器
-- 负责管理和协调多个子心流
-- 维护AI的整体思维状态
-- 定期进行全局思考更新
+1 接受消息,由HeartHC_processor处理消息,存储消息
-### 2. 子心流 (SubHeartflow)
-- 位于 `sub_heartflow.py`
-- 处理具体的对话场景(如群聊)
-- 维护特定场景下的思维状态
-- 通过观察者模式接收和处理信息
-- 能够进行独立的思考和回复判断
+ 1.1 process_message()函数,接受消息
-### 3. 观察系统 (Observation)
-- 位于 `observation.py`
-- 负责收集和处理外部信息
-- 支持多种观察类型(如聊天观察)
-- 对信息进行实时总结和更新
+ 1.2 创建消息对应的聊天流(chat_stream)和子心流(sub_heartflow)
+ 1.3 进行常规消息处理
-## 工作流程
+ 1.4 存储消息 store_message()
-1. 主心流启动并创建必要的子心流
-2. 子心流通过观察者接收外部信息
-3. 系统进行信息处理和思维更新
-4. 根据情感状态和思维结果决定是否回复
-5. 生成合适的回复并更新思维状态
+ 1.5 计算兴趣度Interest
-## 使用说明
+ 1.6 将消息连同兴趣度,存储到内存中的interest_dict(SubHeartflow的属性)
-### 创建新的子心流
-```python
-heartflow = Heartflow()
-subheartflow = heartflow.create_subheartflow(chat_id)
-```
+2 根据 sub_heartflow 的聊天状态,决定后续处理流程
-### 添加观察者
-```python
-observation = ChattingObservation(chat_id)
-subheartflow.add_observation(observation)
-```
+ 2a ABSENT状态:不做任何处理
-## 配置说明
+ 2b CHAT状态:送入NormalChat 实例
-系统的主要配置参数:
-- `sub_heart_flow_stop_time`: 子心流停止时间
-- `sub_heart_flow_freeze_time`: 子心流冻结时间
-- `heart_flow_update_interval`: 心流更新间隔
+ 2c FOCUS状态:送入HeartFChatting 实例
-## 注意事项
+b NormalChat工作方式
-1. 子心流会在长时间不活跃后自动清理
-2. 需要合理配置更新间隔以平衡性能和响应速度
-3. 观察系统会限制消息处理数量以避免过载
+ 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 中移除该消息。
-# HeartFChatting 与主动回复流程说明 (V2)
+c HeartFChatting工作方式
-本文档描述了 `HeartFChatting` 类及其在 `heartFC_controler` 模块中实现的主动、基于兴趣的回复流程。
-
-## 1. `HeartFChatting` 类概述
-
-* **目标**: 管理特定聊天流 (`stream_id`) 的主动回复逻辑,使其行为更像人类的自然交流。
-* **创建时机**: 当 `HeartFC_Chat` 的兴趣监控任务 (`_interest_monitor_loop`) 检测到某个聊天流的兴趣度 (`InterestChatting`) 达到了触发回复评估的条件 (`should_evaluate_reply`) 时,会为该 `stream_id` 获取或创建唯一的 `HeartFChatting` 实例 (`_get_or_create_heartFC_chat`)。
-* **持有**:
- * 对应的 `sub_heartflow` 实例引用 (通过 `heartflow.get_subheartflow(stream_id)`)。
- * 对应的 `chat_stream` 实例引用。
- * 对 `HeartFC_Chat` 单例的引用 (用于调用发送消息、处理表情等辅助方法)。
-* **初始化**: `HeartFChatting` 实例在创建后会执行异步初始化 (`_initialize`),这可能包括加载必要的上下文或历史信息(*待确认是否实现了读取历史消息*)。
-
-## 2. 核心回复流程 (由 `HeartFC_Chat` 触发)
-
-当 `HeartFC_Chat` 调用 `HeartFChatting` 实例的方法 (例如 `add_time`) 时,会启动内部的回复决策与执行流程:
-
-1. **规划 (Planner):**
- * **输入**: 从关联的 `sub_heartflow` 获取观察结果、思考链、记忆片段等上下文信息。
- * **决策**:
- * 判断当前是否适合进行回复。
- * 决定回复的形式(纯文本、带表情包等)。
- * 选择合适的回复时机和策略。
- * **实现**: *此部分逻辑待详细实现,可能利用 LLM 的工具调用能力来增强决策的灵活性和智能性。需要考虑机器人的个性化设定。*
-
-2. **回复生成 (Replier):**
- * **输入**: Planner 的决策结果和必要的上下文。
- * **执行**:
- * 调用 `ResponseGenerator` (`self.gpt`) 或类似组件生成具体的回复文本内容。
- * 可能根据 Planner 的策略生成多个候选回复。
- * **并发**: 系统支持同时存在多个思考/生成任务(上限由 `global_config.max_concurrent_thinking_messages` 控制)。
-
-3. **检查 (Checker):**
- * **时机**: 在回复生成过程中或生成后、发送前执行。
- * **目的**:
- * 检查自开始生成回复以来,聊天流中是否出现了新的消息。
- * 评估已生成的候选回复在新的上下文下是否仍然合适、相关。
- * *需要实现相似度比较逻辑,防止发送与近期消息内容相近或重复的回复。*
- * **处理**: 如果检查结果认为回复不合适,则该回复将被**抛弃**。
-
-4. **发送协调:**
- * **执行**: 如果 Checker 通过,`HeartFChatting` 会调用 `HeartFC_Chat` 实例提供的发送接口:
- * `_create_thinking_message`: 通知 `MessageManager` 显示"正在思考"状态。
- * `_send_response_messages`: 将最终的回复文本交给 `MessageManager` 进行排队和发送。
- * `_handle_emoji`: 如果需要发送表情包,调用此方法处理表情包的获取和发送。
- * **细节**: 实际的消息发送、排队、间隔控制由 `MessageManager` 和 `MessageSender` 负责。
-
-## 3. 与其他模块的交互
-
-* **`HeartFC_Chat`**:
- * 创建、管理和触发 `HeartFChatting` 实例。
- * 提供发送消息 (`_send_response_messages`)、处理表情 (`_handle_emoji`)、创建思考消息 (`_create_thinking_message`) 的接口给 `HeartFChatting` 调用。
- * 运行兴趣监控循环 (`_interest_monitor_loop`)。
-* **`InterestManager` / `InterestChatting`**:
- * `InterestManager` 存储每个 `stream_id` 的 `InterestChatting` 实例。
- * `InterestChatting` 负责计算兴趣衰减和回复概率。
- * `HeartFC_Chat` 查询 `InterestChatting.should_evaluate_reply()` 来决定是否触发 `HeartFChatting`。
-* **`heartflow` / `sub_heartflow`**:
- * `HeartFChatting` 从对应的 `sub_heartflow` 获取进行规划所需的核心上下文信息 (观察、思考链等)。
-* **`MessageManager` / `MessageSender`**:
- * 接收来自 `HeartFC_Chat` 的发送请求 (思考消息、文本消息、表情包消息)。
- * 管理消息队列 (`MessageContainer`),处理消息发送间隔和实际发送 (`MessageSender`)。
-* **`ResponseGenerator` (`gpt`)**:
- * 被 `HeartFChatting` 的 Replier 部分调用,用于生成回复文本。
-* **`MessageStorage`**:
- * 存储所有接收和发送的消息。
-* **`HippocampusManager`**:
- * `HeartFC_Processor` 使用它计算传入消息的记忆激活率,作为兴趣度计算的输入之一。
-
-## 4. 原有问题与状态更新
-
-1. **每个 `pfchating` 是否对应一个 `chat_stream`,是否是唯一的?**
- * **是**。`HeartFC_Chat._get_or_create_heartFC_chat` 确保了每个 `stream_id` 只有一个 `HeartFChatting` 实例。 (已确认)
-2. **`observe_text` 传入进来是纯 str,是不是应该传进来 message 构成的 list?**
- * **机制已改变**。当前的触发机制是基于 `InterestManager` 的概率判断。`HeartFChatting` 启动后,应从其关联的 `sub_heartflow` 获取更丰富的上下文信息,而非简单的 `observe_text`。
-3. **检查失败的回复应该怎么处理?**
- * **暂定:抛弃**。这是当前 Checker 逻辑的基础设定。
-4. **如何比较相似度?**
- * **待实现**。Checker 需要具体的算法来比较候选回复与新消息的相似度。
-5. **Planner 怎么写?**
- * **待实现**。这是 `HeartFChatting` 的核心决策逻辑,需要结合 `sub_heartflow` 的输出、LLM 工具调用和个性化配置来设计。
-
-
-## 6. 未来优化点
-
-* 实现 Checker 中的相似度比较算法。
-* 详细设计并实现 Planner 的决策逻辑,包括 LLM 工具调用和个性化。
-* 确认并完善 `HeartFChatting._initialize()` 中的历史消息加载逻辑。
-* 探索更优的检查失败回复处理策略(例如:重新规划、修改回复等)。
-* 优化 `HeartFChatting` 与 `sub_heartflow` 的信息交互。
+ 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空转。
-BUG:
-2.复读,可能是planner还未校准好
-3.planner还未个性化,需要加入bot个性信息,且获取的聊天内容有问题
\ No newline at end of file
+## 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`, `evaluate_interest_and_promote`)。
+ - **新增**: 通过调用 `evaluate_and_transition_subflows_by_llm` 方法,使用 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` 中的 `evaluate_and_transition_subflows_by_llm` 方法调用大模型(LLM)。LLM 读取该群聊的近期内容和结合自身个性信息,判断是否"想"在该群开始聊天。
+ - **执行**: 若 LLM 判断为是,且名额未满,`SubHeartflowManager` 调用 `change_chat_state(ChatState.CHAT)`。
+ - **`CHAT` -> `FOCUSED` (激活专注)**:
+ - **触发条件**: 子心流处于 `CHAT` 状态,其内部维护的"开屎热聊"概率 (`InterestChatting.start_hfc_probability`) 达到预设阈值(表示对当前聊天兴趣浓厚),同时 `Heartflow` 的主状态允许 `FOCUSED` 模式,且 `FOCUSED` 名额未满。
+ - **判定机制**: `SubHeartflowManager` 中的 `evaluate_interest_and_promote` 方法定期检查满足条件的 `CHAT` 子心流。
+ - **执行**: 若满足所有条件,`SubHeartflowManager` 调用 `change_chat_state(ChatState.FOCUSED)`。
+ - **注意**: 无法从 `ABSENT` 直接跳到 `FOCUSED`,必须先经过 `CHAT`。
+ - **`FOCUSED` -> `ABSENT` (退出专注)**:
+ - **主要途径 (内部驱动)**: 在 `FOCUSED` 状态下运行的 `HeartFlowChatInstance` 连续多次决策为 `no_reply` (例如达到 5 次,次数可配),它会通过回调函数 (`request_absent_transition`) 请求 `SubHeartflowManager` 将其状态**直接**设置为 `ABSENT`。
+ - **其他途径 (外部驱动)**:
+ - `Heartflow` 主状态变为 `OFFLINE`,`SubHeartflowManager` 强制所有子心流变为 `ABSENT`。
+ - `SubHeartflowManager` 因 `FOCUSED` 名额超限 (`enforce_subheartflow_limits`) 或随机停用 (`randomly_deactivate_subflows`) 而将其设置为 `ABSENT`。
+ - **`CHAT` -> `ABSENT` (退出闲聊)**:
+ - **主要途径 (内部驱动)**: `SubHeartflowManager` 中的 `evaluate_and_transition_subflows_by_llm` 方法调用 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)
+ ```
+
diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py
index 85fb6c502..d2bd93213 100644
--- a/src/heart_flow/background_tasks.py
+++ b/src/heart_flow/background_tasks.py
@@ -34,7 +34,6 @@ class BackgroundTaskManager:
update_interval: int,
cleanup_interval: int,
log_interval: int,
- inactive_threshold: int,
# 新增兴趣评估间隔参数
interest_eval_interval: int = INTEREST_EVAL_INTERVAL_SECONDS,
# 新增随机停用间隔参数
@@ -49,7 +48,6 @@ class BackgroundTaskManager:
self.update_interval = update_interval
self.cleanup_interval = cleanup_interval
self.log_interval = log_interval
- self.inactive_threshold = inactive_threshold # For cleanup task
self.interest_eval_interval = interest_eval_interval # 存储兴趣评估间隔
self.random_deactivation_interval = random_deactivation_interval # 存储随机停用间隔
@@ -59,6 +57,7 @@ class BackgroundTaskManager:
self._logging_task: Optional[asyncio.Task] = None
self._interest_eval_task: Optional[asyncio.Task] = None # 新增兴趣评估任务引用
self._random_deactivation_task: Optional[asyncio.Task] = None # 新增随机停用任务引用
+ self._hf_judge_state_update_task: Optional[asyncio.Task] = None # 新增状态评估任务引用
self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks
async def start_tasks(self):
@@ -80,12 +79,20 @@ class BackgroundTaskManager:
f"聊天状态更新任务已启动 间隔:{self.update_interval}s",
"_state_update_task",
),
+ (
+ self._hf_judge_state_update_task,
+ lambda: self._run_hf_judge_state_update_cycle(60),
+ "hf_judge_state_update",
+ "debug",
+ f"状态评估任务已启动 间隔:{60}s",
+ "_hf_judge_state_update_task",
+ ),
(
self._cleanup_task,
self._run_cleanup_cycle,
"hf_cleanup",
"info",
- f"清理任务已启动 间隔:{self.cleanup_interval}s 阈值:{self.inactive_threshold}s",
+ f"清理任务已启动 间隔:{self.cleanup_interval}s",
"_cleanup_task",
),
(
@@ -105,15 +112,6 @@ class BackgroundTaskManager:
f"兴趣评估任务已启动 间隔:{self.interest_eval_interval}s",
"_interest_eval_task",
),
- # 新增随机停用任务配置
- (
- self._random_deactivation_task,
- self._run_random_deactivation_cycle,
- "hf_random_deactivation",
- "debug", # 设为debug,避免过多日志
- f"随机停用任务已启动 间隔:{self.random_deactivation_interval}s",
- "_random_deactivation_task",
- ),
]
# 统一启动所有任务
@@ -204,34 +202,46 @@ class BackgroundTaskManager:
if state_changed:
current_state = self.mai_state_info.get_current_state()
- await self.subheartflow_manager.enforce_subheartflow_limits(current_state)
+ await self.subheartflow_manager.enforce_subheartflow_limits()
# 状态转换处理
+
if (
- previous_status == self.mai_state_info.mai_status.OFFLINE
- and current_state != self.mai_state_info.mai_status.OFFLINE
- ):
- logger.info("[后台任务] 主状态激活,触发子流激活")
- await self.subheartflow_manager.activate_random_subflows_to_chat(current_state)
- elif (
current_state == self.mai_state_info.mai_status.OFFLINE
and previous_status != self.mai_state_info.mai_status.OFFLINE
):
- logger.info("[后台任务] 主状态离线,触发子流停用")
+ logger.info("检测到离线,停用所有子心流")
await self.subheartflow_manager.deactivate_all_subflows()
+ async def _perform_hf_judge_state_update_work(self):
+ """调用llm检测是否转换ABSENT-CHAT状态"""
+ logger.info("[状态评估任务] 开始基于LLM评估子心流状态...")
+ await self.subheartflow_manager.evaluate_and_transition_subflows_by_llm()
+
async def _perform_cleanup_work(self):
- """执行一轮子心流清理操作。"""
- flows_to_stop = self.subheartflow_manager.cleanup_inactive_subheartflows(self.inactive_threshold)
- if flows_to_stop:
- logger.info(f"[Background Task Cleanup] Attempting to stop {len(flows_to_stop)} inactive flows...")
- stopped_count = 0
- for flow_id, reason in flows_to_stop:
- if await self.subheartflow_manager.stop_subheartflow(flow_id, f"定期清理: {reason}"):
- stopped_count += 1
- logger.info(f"[Background Task Cleanup] Cleanup cycle finished. Stopped {stopped_count} inactive flows.")
- else:
- logger.debug("[Background Task Cleanup] Cleanup cycle finished. No inactive flows found.")
+ """执行子心流清理任务
+ 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 _perform_logging_work(self):
"""执行一轮状态日志记录。"""
@@ -241,15 +251,10 @@ class BackgroundTaskManager:
async def _perform_interest_eval_work(self):
"""执行一轮子心流兴趣评估与提升检查。"""
# 直接调用 subheartflow_manager 的方法,并传递当前状态信息
- await self.subheartflow_manager.evaluate_interest_and_promote(self.mai_state_info)
+ await self.subheartflow_manager.evaluate_interest_and_promote()
# --- 结束新增 ---
- # --- 新增随机停用工作函数 ---
- async def _perform_random_deactivation_work(self):
- """执行一轮子心流随机停用检查。"""
- await self.subheartflow_manager.randomly_deactivate_subflows()
-
# --- 结束新增 ---
# --- Specific Task Runners --- #
@@ -258,6 +263,11 @@ class BackgroundTaskManager:
task_name="State Update", interval=interval, task_func=self._perform_state_update_work
)
+ async def _run_hf_judge_state_update_cycle(self, interval: int):
+ await self._run_periodic_loop(
+ task_name="State Update", interval=interval, task_func=self._perform_hf_judge_state_update_work
+ )
+
async def _run_cleanup_cycle(self):
await self._run_periodic_loop(
task_name="Subflow Cleanup", interval=self.cleanup_interval, task_func=self._perform_cleanup_work
@@ -275,16 +285,3 @@ class BackgroundTaskManager:
interval=self.interest_eval_interval,
task_func=self._perform_interest_eval_work,
)
-
- # --- 结束新增 ---
-
- # --- 新增随机停用任务运行器 ---
- async def _run_random_deactivation_cycle(self):
- """运行随机停用循环。"""
- await self._run_periodic_loop(
- task_name="Random Deactivation",
- interval=self.random_deactivation_interval,
- task_func=self._perform_random_deactivation_work,
- )
-
- # --- 结束新增 ---
diff --git a/src/heart_flow/chat_state_info.py b/src/heart_flow/chat_state_info.py
index 14fd33403..619f372fc 100644
--- a/src/heart_flow/chat_state_info.py
+++ b/src/heart_flow/chat_state_info.py
@@ -5,7 +5,7 @@ import enum
class ChatState(enum.Enum):
ABSENT = "没在看群"
CHAT = "随便水群"
- FOCUSED = "激情水群"
+ FOCUSED = "认真水群"
class ChatStateInfo:
diff --git a/src/heart_flow/heartflow.py b/src/heart_flow/heartflow.py
index 7fbc0f58a..a0fb8e4fa 100644
--- a/src/heart_flow/heartflow.py
+++ b/src/heart_flow/heartflow.py
@@ -47,8 +47,8 @@ class Heartflow:
self.current_state: MaiStateInfo = MaiStateInfo() # 当前状态信息
self.mai_state_manager: MaiStateManager = MaiStateManager() # 状态决策管理器
- # 子心流管理
- self.subheartflow_manager: SubHeartflowManager = SubHeartflowManager() # 子心流管理器
+ # 子心流管理 (在初始化时传入 current_state)
+ self.subheartflow_manager: SubHeartflowManager = SubHeartflowManager(self.current_state)
# LLM模型配置
self.llm_model = LLMRequest(
@@ -72,26 +72,19 @@ class Heartflow:
update_interval=STATE_UPDATE_INTERVAL_SECONDS,
cleanup_interval=CLEANUP_INTERVAL_SECONDS,
log_interval=3, # Example: Using value directly, ideally get from config
- inactive_threshold=INACTIVE_THRESHOLD_SECONDS,
)
- async def create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:
+ async def get_or_create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:
"""获取或创建一个新的SubHeartflow实例 - 委托给 SubHeartflowManager"""
- return await self.subheartflow_manager.create_or_get_subheartflow(subheartflow_id, self.current_state)
-
- def get_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:
- """获取指定ID的SubHeartflow实例"""
- return self.subheartflow_manager.get_subheartflow(subheartflow_id)
-
- def get_all_subheartflows_streams_ids(self) -> list[Any]:
- """获取当前所有活跃的子心流的 ID 列表 - 委托给 SubHeartflowManager"""
- return self.subheartflow_manager.get_all_subheartflows_ids()
+ # 不再需要传入 self.current_state
+ return await self.subheartflow_manager.get_or_create_subheartflow(subheartflow_id)
async def heartflow_start_working(self):
"""启动后台任务"""
await self.background_task_manager.start_tasks()
logger.info("[Heartflow] 后台任务已启动")
+ # 根本不会用到这个函数吧,那样麦麦直接死了
async def stop_working(self):
"""停止所有任务和子心流"""
logger.info("[Heartflow] 正在停止任务和子心流...")
diff --git a/src/heart_flow/interest_logger.py b/src/heart_flow/interest_logger.py
index 05a7da39d..d4e746e63 100644
--- a/src/heart_flow/interest_logger.py
+++ b/src/heart_flow/interest_logger.py
@@ -54,11 +54,11 @@ class InterestLogger:
results = {}
if not all_flows:
- logger.debug("未找到任何子心流状态")
+ # logger.debug("未找到任何子心流状态")
return results
for subheartflow in all_flows:
- if self.subheartflow_manager.get_subheartflow(subheartflow.subheartflow_id):
+ if await self.subheartflow_manager.get_or_create_subheartflow(subheartflow.subheartflow_id):
tasks.append(
asyncio.create_task(subheartflow.get_full_state(), name=f"get_state_{subheartflow.subheartflow_id}")
)
@@ -109,7 +109,7 @@ class InterestLogger:
}
if not all_subflow_states:
- logger.debug("没有获取到任何子心流状态,仅记录主心流状态")
+ # logger.debug("没有获取到任何子心流状态,仅记录主心流状态")
with open(self._history_log_file_path, "a", encoding="utf-8") as f:
f.write(json.dumps(log_entry_base, ensure_ascii=False) + "\n")
return
diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py
index 6f645f670..4341e3ded 100644
--- a/src/heart_flow/mai_state_manager.py
+++ b/src/heart_flow/mai_state_manager.py
@@ -22,14 +22,14 @@ class MaiState(enum.Enum):
聊天状态:
OFFLINE: 不在线:回复概率极低,不会进行任何聊天
PEEKING: 看一眼手机:回复概率较低,会进行一些普通聊天
- NORMAL_CHAT: 正常聊天:回复概率较高,会进行一些普通聊天和少量的专注聊天
+ NORMAL_CHAT: 正常看手机:回复概率较高,会进行一些普通聊天和少量的专注聊天
FOCUSED_CHAT: 专注聊天:回复概率极高,会进行专注聊天和少量的普通聊天
"""
OFFLINE = "不在线"
- PEEKING = "看一眼"
- NORMAL_CHAT = "正常聊天"
- FOCUSED_CHAT = "专心聊天"
+ PEEKING = "看一眼手机"
+ NORMAL_CHAT = "正常看手机"
+ FOCUSED_CHAT = "专心看手机"
def get_normal_chat_max_num(self):
# 调试用
@@ -39,11 +39,11 @@ class MaiState(enum.Enum):
if self == MaiState.OFFLINE:
return 0
elif self == MaiState.PEEKING:
- return 2
+ return 30
elif self == MaiState.NORMAL_CHAT:
- return 3
+ return 40
elif self == MaiState.FOCUSED_CHAT:
- return 2
+ return 30
def get_focused_chat_max_num(self):
# 调试用
@@ -53,11 +53,11 @@ class MaiState(enum.Enum):
if self == MaiState.OFFLINE:
return 0
elif self == MaiState.PEEKING:
- return 1
+ return 20
elif self == MaiState.NORMAL_CHAT:
- return 1
+ return 30
elif self == MaiState.FOCUSED_CHAT:
- return 3
+ return 40
class MaiStateInfo:
@@ -137,11 +137,11 @@ class MaiStateManager:
if current_status == MaiState.OFFLINE:
logger.info("当前[离线],没看手机,思考要不要上线看看......")
elif current_status == MaiState.PEEKING:
- logger.info("当前[看一眼],思考要不要继续聊下去......")
+ logger.info("当前[看一眼手机],思考要不要继续聊下去......")
elif current_status == MaiState.NORMAL_CHAT:
- logger.info("当前在[正常聊天]思考要不要继续聊下去......")
+ logger.info("当前在[正常看手机]思考要不要继续聊下去......")
elif current_status == MaiState.FOCUSED_CHAT:
- logger.info("当前在[专心聊天]思考要不要继续聊下去......")
+ logger.info("当前在[专心看手机]思考要不要继续聊下去......")
# 1. 麦麦每分钟都有概率离线
if time_since_last_min_check >= 60:
diff --git a/src/heart_flow/mind.py b/src/heart_flow/mind.py
index e806d18ae..a40ee6ef7 100644
--- a/src/heart_flow/mind.py
+++ b/src/heart_flow/mind.py
@@ -1,7 +1,7 @@
import traceback
from typing import TYPE_CHECKING
-from src.common.logger import get_module_logger
+from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_MIND_STYLE_CONFIG
from src.plugins.models.utils_model import LLMRequest
from src.individuality.individuality import Individuality
from src.plugins.utils.prompt_builder import global_prompt_manager
@@ -12,7 +12,12 @@ if TYPE_CHECKING:
from src.heart_flow.subheartflow_manager import SubHeartflowManager
from src.heart_flow.mai_state_manager import MaiStateInfo
-logger = get_module_logger("mind")
+mind_log_config = LogConfig(
+ console_format=SUB_HEARTFLOW_MIND_STYLE_CONFIG["console_format"],
+ file_format=SUB_HEARTFLOW_MIND_STYLE_CONFIG["file_format"],
+)
+
+logger = get_module_logger("mind", config=mind_log_config)
class Mind:
diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py
index 9391a660a..b960154ca 100644
--- a/src/heart_flow/observation.py
+++ b/src/heart_flow/observation.py
@@ -23,6 +23,9 @@ class Observation:
self.observe_id = observe_id
self.last_observe_time = datetime.now().timestamp() # 初始化为当前时间
+ async def observe(self):
+ pass
+
# 聊天观察
class ChattingObservation(Observation):
diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py
index 7a6e009c7..6ea60ea1f 100644
--- a/src/heart_flow/sub_heartflow.py
+++ b/src/heart_flow/sub_heartflow.py
@@ -2,7 +2,7 @@ from .observation import Observation, ChattingObservation
import asyncio
from src.config.config import global_config
import time
-from typing import Optional, List, Dict, Callable
+from typing import Optional, List, Dict, Tuple, Callable, Coroutine
import traceback
from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_STYLE_CONFIG # noqa: E402
import random
@@ -15,6 +15,11 @@ from src.heart_flow.mai_state_manager import MaiStateInfo
from src.heart_flow.chat_state_info import ChatState, ChatStateInfo
from src.heart_flow.sub_mind import SubMind
+# # --- REMOVE: Conditional import --- #
+# if TYPE_CHECKING:
+# from src.heart_flow.subheartflow_manager import SubHeartflowManager
+# # --- END REMOVE --- #
+
# 定义常量 (从 interest.py 移动过来)
MAX_INTEREST = 15.0
@@ -26,13 +31,6 @@ subheartflow_config = LogConfig(
)
logger = get_module_logger("subheartflow", config=subheartflow_config)
-interest_log_config = LogConfig(
- console_format=SUB_HEARTFLOW_STYLE_CONFIG["console_format"],
- file_format=SUB_HEARTFLOW_STYLE_CONFIG["file_format"],
-)
-interest_logger = get_module_logger("InterestChatting", config=interest_log_config)
-
-
base_reply_probability = 0.05
probability_increase_rate_per_second = 0.08
max_reply_probability = 1
@@ -48,8 +46,8 @@ class InterestChatting:
increase_rate=probability_increase_rate_per_second,
decay_factor=global_config.probability_decay_factor_per_second,
max_probability=max_reply_probability,
- state_change_callback: Optional[Callable[[ChatState], None]] = None,
):
+ # 基础属性初始化
self.interest_level: float = 0.0
self.last_update_time: float = time.time()
self.decay_rate_per_second: float = decay_rate
@@ -63,20 +61,57 @@ class InterestChatting:
self.max_reply_probability: float = max_probability
self.current_reply_probability: float = 0.0
self.is_above_threshold: bool = False
+
+ # 任务相关属性初始化
self.update_task: Optional[asyncio.Task] = None
self._stop_event = asyncio.Event()
+ self._task_lock = asyncio.Lock()
+ self._is_running = False
self.interest_dict: Dict[str, tuple[MessageRecv, float, bool]] = {}
self.update_interval = 1.0
- self.start_updates(self.update_interval) # 初始化时启动后台更新任务
self.above_threshold = False
self.start_hfc_probability = 0.0
+ async def initialize(self):
+ async with self._task_lock:
+ if self._is_running:
+ logger.debug("后台兴趣更新任务已在运行中。")
+ return
+
+ # 清理已完成或已取消的任务
+ if self.update_task and (self.update_task.done() or self.update_task.cancelled()):
+ self.update_task = None
+
+ if not self.update_task:
+ self._stop_event.clear()
+ self._is_running = True
+ self.update_task = asyncio.create_task(self._run_update_loop(self.update_interval))
+ logger.debug("后台兴趣更新任务已创建并启动。")
+
def add_interest_dict(self, message: MessageRecv, interest_value: float, is_mentioned: bool):
+ """添加消息到兴趣字典
+
+ 参数:
+ message: 接收到的消息
+ interest_value: 兴趣值
+ is_mentioned: 是否被提及
+
+ 功能:
+ 1. 将消息添加到兴趣字典
+ 2. 更新最后交互时间
+ 3. 如果字典长度超过10,删除最旧的消息
+ """
+ # 添加新消息
self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned)
self.last_interaction_time = time.time()
+ # 如果字典长度超过10,删除最旧的消息
+ if len(self.interest_dict) > 10:
+ oldest_key = next(iter(self.interest_dict))
+ self.interest_dict.pop(oldest_key)
+
async def _calculate_decay(self):
"""计算兴趣值的衰减
@@ -97,7 +132,7 @@ class InterestChatting:
# 异常情况处理
if self.decay_rate_per_second <= 0:
- interest_logger.warning(f"衰减率({self.decay_rate_per_second})无效,重置兴趣值为0")
+ logger.warning(f"衰减率({self.decay_rate_per_second})无效,重置兴趣值为0")
self.interest_level = 0.0
return
@@ -106,7 +141,7 @@ class InterestChatting:
decay_factor = math.pow(self.decay_rate_per_second, self.update_interval)
self.interest_level *= decay_factor
except ValueError as e:
- interest_logger.error(
+ logger.error(
f"衰减计算错误: {e} 参数: 衰减率={self.decay_rate_per_second} 时间差={self.update_interval} 当前兴趣={self.interest_level}"
)
self.interest_level = 0.0
@@ -116,14 +151,14 @@ class InterestChatting:
if self.above_threshold:
self.start_hfc_probability += 0.1
else:
- if self.start_hfc_probability != 0:
- self.start_hfc_probability -= 0.1
+ if self.start_hfc_probability > 0:
+ self.start_hfc_probability = max(0, self.start_hfc_probability - 0.1)
- async def increase_interest(self, current_time: float, value: float):
+ async def increase_interest(self, value: float):
self.interest_level += value
self.interest_level = min(self.interest_level, self.max_interest)
- async def decrease_interest(self, current_time: float, value: float):
+ async def decrease_interest(self, value: float):
self.interest_level -= value
self.interest_level = max(self.interest_level, 0.0)
@@ -135,7 +170,7 @@ class InterestChatting:
return {
"interest_level": round(interest, 2),
"start_hfc_probability": round(self.start_hfc_probability, 4),
- "is_above_threshold": self.is_above_threshold,
+ "above_threshold": self.above_threshold,
}
async def should_evaluate_reply(self) -> bool:
@@ -148,114 +183,129 @@ class InterestChatting:
# --- 新增后台更新任务相关方法 ---
async def _run_update_loop(self, update_interval: float = 1.0):
"""后台循环,定期更新兴趣和回复概率。"""
- while not self._stop_event.is_set():
- try:
- if self.interest_level != 0:
- await self._calculate_decay()
+ try:
+ while not self._stop_event.is_set():
+ try:
+ if self.interest_level != 0:
+ await self._calculate_decay()
- await self._update_reply_probability()
+ await self._update_reply_probability()
- # 等待下一个周期或停止事件
- await asyncio.wait_for(self._stop_event.wait(), timeout=update_interval)
- except asyncio.TimeoutError:
- # 正常超时,继续循环
- continue
- except asyncio.CancelledError:
- interest_logger.info("InterestChatting 更新循环被取消。")
- break
- except Exception as e:
- interest_logger.error(f"InterestChatting 更新循环出错: {e}")
- interest_logger.error(traceback.format_exc())
- # 防止错误导致CPU飙升,稍作等待
- await asyncio.sleep(5)
- interest_logger.info("InterestChatting 更新循环已停止。")
-
- def start_updates(self, update_interval: float = 1.0):
- """启动后台更新任务"""
- if self.update_task is None or self.update_task.done():
- self._stop_event.clear()
- self.update_task = asyncio.create_task(self._run_update_loop(update_interval))
- interest_logger.debug("后台兴趣更新任务已创建并启动。")
- else:
- interest_logger.debug("后台兴趣更新任务已在运行中。")
+ # 等待下一个周期或停止事件
+ await asyncio.wait_for(self._stop_event.wait(), timeout=update_interval)
+ except asyncio.TimeoutError:
+ # 正常超时,继续循环
+ continue
+ except Exception as e:
+ logger.error(f"InterestChatting 更新循环出错: {e}")
+ logger.error(traceback.format_exc())
+ # 防止错误导致CPU飙升,稍作等待
+ await asyncio.sleep(5)
+ except asyncio.CancelledError:
+ logger.info("InterestChatting 更新循环被取消。")
+ finally:
+ self._is_running = False
+ logger.info("InterestChatting 更新循环已停止。")
async def stop_updates(self):
- """停止后台更新任务"""
- if self.update_task and not self.update_task.done():
- interest_logger.info("正在停止 InterestChatting 后台更新任务...")
- self._stop_event.set() # 发送停止信号
- try:
- # 等待任务结束,设置超时
- await asyncio.wait_for(self.update_task, timeout=5.0)
- interest_logger.info("InterestChatting 后台更新任务已成功停止。")
- except asyncio.TimeoutError:
- interest_logger.warning("停止 InterestChatting 后台任务超时,尝试取消...")
- self.update_task.cancel()
+ """停止后台更新任务,使用锁确保并发安全"""
+ async with self._task_lock:
+ if not self._is_running:
+ logger.debug("后台兴趣更新任务未运行。")
+ return
+
+ logger.info("正在停止 InterestChatting 后台更新任务...")
+ self._stop_event.set()
+
+ if self.update_task and not self.update_task.done():
try:
- await self.update_task # 等待取消完成
- except asyncio.CancelledError:
- interest_logger.info("InterestChatting 后台更新任务已被取消。")
- except Exception as e:
- interest_logger.error(f"停止 InterestChatting 后台任务时发生异常: {e}")
- finally:
- self.update_task = None
- else:
- interest_logger.debug("InterestChatting 后台更新任务未运行或已完成。")
+ # 等待任务结束,设置超时
+ await asyncio.wait_for(self.update_task, timeout=5.0)
+ logger.info("InterestChatting 后台更新任务已成功停止。")
+ except asyncio.TimeoutError:
+ logger.warning("停止 InterestChatting 后台任务超时,尝试取消...")
+ self.update_task.cancel()
+ try:
+ await self.update_task # 等待取消完成
+ except asyncio.CancelledError:
+ logger.info("InterestChatting 后台更新任务已被取消。")
+ except Exception as e:
+ logger.error(f"停止 InterestChatting 后台任务时发生异常: {e}")
+ finally:
+ self.update_task = None
+ self._is_running = False
# --- 结束 新增方法 ---
class SubHeartflow:
- def __init__(self, subheartflow_id, mai_states: MaiStateInfo):
+ def __init__(
+ self,
+ subheartflow_id,
+ mai_states: MaiStateInfo,
+ hfc_no_reply_callback: Callable[[], Coroutine[None, None, None]],
+ ):
"""子心流初始化函数
Args:
subheartflow_id: 子心流唯一标识符
- parent_heartflow: 父级心流实例
+ mai_states: 麦麦状态信息实例
+ hfc_no_reply_callback: HFChatting 连续不回复时触发的回调
"""
- # 基础属性
+ # 基础属性,两个值是一样的
self.subheartflow_id = subheartflow_id
self.chat_id = subheartflow_id
+ self.hfc_no_reply_callback = hfc_no_reply_callback
+ # 麦麦的状态
self.mai_states = mai_states
- # 聊天状态管理
- self.chat_state: ChatStateInfo = ChatStateInfo() # 该sub_heartflow的聊天状态信息
- self.interest_chatting = InterestChatting(state_change_callback=self.set_chat_state)
+ # 这个聊天流的状态
+ 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.interest_chatting: InterestChatting = InterestChatting()
# 活动状态管理
- self.last_active_time = time.time() # 最后活跃时间
self.should_stop = False # 停止标志
self.task: Optional[asyncio.Task] = None # 后台任务
+
+ # 随便水群 normal_chat 和 认真水群 heartFC_chat 实例
+ # CHAT模式激活 随便水群 FOCUS模式激活 认真水群
self.heart_fc_instance: Optional[HeartFChatting] = None # 该sub_heartflow的HeartFChatting实例
self.normal_chat_instance: Optional[NormalChat] = None # 该sub_heartflow的NormalChat实例
- # 观察和知识系统
+ # 观察,目前只有聊天观察,可以载入多个
+ # 负责对处理过的消息进行观察
self.observations: List[ChattingObservation] = [] # 观察列表
- self.running_knowledges = [] # 运行中的知识
+ # self.running_knowledges = [] # 运行中的知识,待完善
- # LLM模型配置
+ # LLM模型配置,负责进行思考
self.sub_mind = SubMind(
subheartflow_id=self.subheartflow_id, chat_state=self.chat_state, observations=self.observations
)
+ # 日志前缀
self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id
- async def add_time_current_state(self, add_time: float):
- self.current_state_time += add_time
+ async def initialize(self):
+ """异步初始化方法,创建兴趣流"""
+ await self.interest_chatting.initialize()
+ logger.debug(f"{self.log_prefix} InterestChatting 实例已初始化。")
- async def change_to_state_chat(self):
- self.current_state_time = 120
- self._start_normal_chat()
-
- async def change_to_state_focused(self):
- self.current_state_time = 60
- self._start_heart_fc_chat()
+ 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 的兴趣监控"""
+ """
+ 停止 NormalChat 实例
+ 切出 CHAT 状态时使用
+ """
if self.normal_chat_instance:
- logger.info(f"{self.log_prefix} 停止 NormalChat 兴趣监控...")
+ logger.info(f"{self.log_prefix} 离开CHAT模式,结束 随便水群")
try:
await self.normal_chat_instance.stop_chat() # 调用 stop_chat
except Exception as e:
@@ -263,23 +313,21 @@ class SubHeartflow:
logger.error(traceback.format_exc())
async def _start_normal_chat(self) -> bool:
- """启动 NormalChat 实例及其兴趣监控,确保 HeartFChatting 已停止"""
- await self._stop_heart_fc_chat() # 确保专注聊天已停止
+ """
+ 启动 NormalChat 实例,
+ 进入 CHAT 状态时使用
+
+ 确保 HeartFChatting 已停止
+ """
+ await self._stop_heart_fc_chat() # 确保 专注聊天已停止
log_prefix = self.log_prefix
try:
- # 总是尝试创建或获取最新的 stream 和 interest_dict
+ # 获取聊天流并创建 NormalChat 实例
chat_stream = chat_manager.get_stream(self.chat_id)
- if not chat_stream:
- logger.error(f"{log_prefix} 无法获取 chat_stream,无法启动 NormalChat。")
- return False
-
- # 如果实例不存在或需要更新,则创建新实例
- # if not self.normal_chat_instance: # 或者总是重新创建以获取最新的 interest_dict?
self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict())
- logger.info(f"{log_prefix} 创建或更新 NormalChat 实例。")
- logger.info(f"{log_prefix} 启动 NormalChat 兴趣监控...")
+ logger.info(f"{log_prefix} 启动 NormalChat 随便水群...")
await self.normal_chat_instance.start_chat() # <--- 修正:调用 start_chat
return True
except Exception as e:
@@ -328,11 +376,17 @@ class SubHeartflow:
# 如果实例不存在,则创建并启动
logger.info(f"{log_prefix} 麦麦准备开始专注聊天 (创建新实例)...")
try:
+ # 创建 HeartFChatting 实例,并传递 从构造函数传入的 回调函数
self.heart_fc_instance = HeartFChatting(
- chat_id=self.chat_id, sub_mind=self.sub_mind, observations=self.observations
+ chat_id=self.subheartflow_id,
+ sub_mind=self.sub_mind,
+ observations=self.observations, # 传递所有观察者
+ on_consecutive_no_reply_callback=self.hfc_no_reply_callback, # <-- Use stored callback
)
+
+ # 初始化并启动 HeartFChatting
if await self.heart_fc_instance._initialize():
- await self.heart_fc_instance.start() # 初始化成功后启动循环
+ await self.heart_fc_instance.start()
logger.info(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。")
return True
else:
@@ -345,55 +399,38 @@ class SubHeartflow:
self.heart_fc_instance = None # 创建或初始化异常,清理实例
return False
- async def set_chat_state(self, new_state: "ChatState", current_states_num: tuple = ()):
+ async def change_chat_state(self, new_state: "ChatState"):
"""更新sub_heartflow的聊天状态,并管理 HeartFChatting 和 NormalChat 实例及任务"""
current_state = self.chat_state.chat_status
+
if current_state == new_state:
- # logger.trace(f"{self.log_prefix} 状态已为 {current_state.value}, 无需更改。") # 减少日志噪音
return
log_prefix = self.log_prefix
- current_mai_state = self.mai_states.get_current_state()
state_changed = False # 标记状态是否实际发生改变
# --- 状态转换逻辑 ---
if new_state == ChatState.CHAT:
- normal_limit = current_mai_state.get_normal_chat_max_num()
- current_chat_count = current_states_num[1] if len(current_states_num) > 1 else 0
-
- if current_chat_count >= normal_limit and current_state != ChatState.CHAT:
- logger.debug(
- f"{log_prefix} 无法从 {current_state.value} 转到 聊天。原因:聊不过来了 ({current_chat_count}/{normal_limit})"
- )
- return # 阻止状态转换
+ # 移除限额检查逻辑
+ logger.debug(f"{log_prefix} 准备进入或保持 聊天 状态")
+ if await self._start_normal_chat():
+ logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。")
+ state_changed = True
else:
- logger.debug(f"{log_prefix} 准备进入或保持 聊天 状态 ({current_chat_count}/{normal_limit})")
- if await self._start_normal_chat():
- logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。")
- state_changed = True
- else:
- logger.error(f"{log_prefix} 启动 NormalChat 失败,无法进入 CHAT 状态。")
- # 考虑是否需要回滚状态或采取其他措施
- return # 启动失败,不改变状态
+ logger.error(f"{log_prefix} 启动 NormalChat 失败,无法进入 CHAT 状态。")
+ # 考虑是否需要回滚状态或采取其他措施
+ return # 启动失败,不改变状态
elif new_state == ChatState.FOCUSED:
- focused_limit = current_mai_state.get_focused_chat_max_num()
- current_focused_count = current_states_num[2] if len(current_states_num) > 2 else 0
-
- if current_focused_count >= focused_limit and current_state != ChatState.FOCUSED:
- logger.debug(
- f"{log_prefix} 无法从 {current_state.value} 转到 专注。原因:聊不过来了 ({current_focused_count}/{focused_limit})"
- )
- return # 阻止状态转换
+ # 移除限额检查逻辑
+ logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态")
+ if await self._start_heart_fc_chat():
+ logger.info(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。")
+ state_changed = True
else:
- logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态 ({current_focused_count}/{focused_limit})")
- if await self._start_heart_fc_chat():
- logger.info(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。")
- state_changed = True
- else:
- logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。")
- # 启动失败,状态回滚到之前的状态或ABSENT?这里保持不改变
- return # 启动失败,不改变状态
+ logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。")
+ # 启动失败,状态回滚到之前的状态或ABSENT?这里保持不改变
+ return # 启动失败,不改变状态
elif new_state == ChatState.ABSENT:
logger.info(f"{log_prefix} 进入 ABSENT 状态,停止所有聊天活动...")
@@ -403,9 +440,16 @@ class SubHeartflow:
# --- 更新状态和最后活动时间 ---
if state_changed:
- logger.info(f"{log_prefix} 麦麦的聊天状态从 {current_state.value} 变更为 {new_state.value}")
+ self.update_last_chat_state_time()
+ self.history_chat_state.append((current_state, self.chat_state_last_time))
+
+ logger.info(
+ f"{log_prefix} 麦麦的聊天状态从 {current_state.value} (持续了 {self.chat_state_last_time} 秒) 变更为 {new_state.value}"
+ )
+
self.chat_state.chat_status = new_state
- self.last_active_time = time.time()
+ self.chat_state_last_time = 0
+ self.chat_state_changed_time = time.time()
else:
# 如果因为某些原因(如启动失败)没有成功改变状态,记录一下
logger.debug(
@@ -419,7 +463,7 @@ class SubHeartflow:
- 负责子心流的主要后台循环
- 每30秒检查一次停止标志
"""
- logger.info(f"{self.log_prefix} 子心流开始工作...")
+ logger.trace(f"{self.log_prefix} 子心流开始工作...")
while not self.should_stop:
await asyncio.sleep(30) # 30秒检查一次停止标志
@@ -460,9 +504,6 @@ class SubHeartflow:
async def should_evaluate_reply(self) -> bool:
return await self.interest_chatting.should_evaluate_reply()
- async def add_interest_dict_entry(self, message: MessageRecv, interest_value: float, is_mentioned: bool):
- self.interest_chatting.add_interest_dict(message, interest_value, is_mentioned)
-
def get_interest_dict(self) -> Dict[str, tuple[MessageRecv, float, bool]]:
return self.interest_chatting.interest_dict
@@ -476,7 +517,7 @@ class SubHeartflow:
"interest_state": interest_state,
"current_mind": self.sub_mind.current_mind,
"chat_state": self.chat_state.chat_status.value,
- "last_active_time": self.last_active_time,
+ "chat_state_changed_time": self.chat_state_changed_time,
}
async def shutdown(self):
diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py
index c7baa91ed..f340b717b 100644
--- a/src/heart_flow/sub_mind.py
+++ b/src/heart_flow/sub_mind.py
@@ -10,7 +10,8 @@ from ..plugins.utils.prompt_builder import Prompt, global_prompt_manager
from src.do_tool.tool_use import ToolUser
from src.plugins.utils.json_utils import safe_json_dumps, normalize_llm_response, process_llm_tool_calls
from src.heart_flow.chat_state_info import ChatStateInfo
-
+from src.plugins.chat.chat_stream import chat_manager
+from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
subheartflow_config = LogConfig(
console_format=SUB_HEARTFLOW_STYLE_CONFIG["console_format"],
@@ -21,25 +22,34 @@ logger = get_module_logger("subheartflow", config=subheartflow_config)
def init_prompt():
prompt = ""
- # prompt += f"麦麦的总体想法是:{self.main_heartflow_info}\n\n"
prompt += "{extra_info}\n"
- # prompt += "{prompt_schedule}\n"
- # prompt += "{relation_prompt_all}\n"
prompt += "{prompt_personality}\n"
- prompt += "刚刚你的想法是:\n我是{bot_name},我想,{current_thinking_info}\n"
- prompt += "-----------------------------------\n"
- prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:\n{chat_observe_info}\n"
+ prompt += "{last_loop_prompt}\n"
+ prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:\n{chat_observe_info}\n"
prompt += "\n你现在{mood_info}\n"
- prompt += "现在请你生成你的内心想法,要求思考群里正在进行的话题,之前大家聊过的话题,群里成员的关系。"
- prompt += "请你思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复\n"
- prompt += "回复的要求是:平淡一些,简短一些,如果你要回复,最好只回复一个人的一个话题\n"
- prompt += "请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要回复自己的发言\n"
- prompt += "现在请你先输出想法,{hf_do_next},不要分点输出,文字不要浮夸"
- prompt += "在输出完想法后,请你思考应该使用什么工具。工具可以帮你取得一些你不知道的信息,或者进行一些操作。"
- prompt += "如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。\n"
+ prompt += (
+ "请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。"
+ )
+ prompt += "思考并输出你的内心想法\n"
+ prompt += "输出要求:\n"
+ prompt += "1. 根据聊天内容生成你的想法,{hf_do_next}\n"
+ prompt += "2. 不要分点、不要使用表情符号\n"
+ prompt += "3. 避免多余符号(冒号、引号、括号等)\n"
+ prompt += "4. 语言简洁自然,不要浮夸\n"
+ prompt += "5. 如果你刚发言,并且没有人回复你,不要回复\n"
+ prompt += "工具使用说明:\n"
+ prompt += "1. 输出想法后考虑是否需要使用工具\n"
+ prompt += "2. 工具可获取信息或执行操作\n"
+ prompt += "3. 如需处理消息或回复,请使用工具\n"
Prompt(prompt, "sub_heartflow_prompt_before")
+ prompt = ""
+ prompt += "刚刚你的内心想法是:{current_thinking_info}\n"
+ prompt += "{if_replan_prompt}\n"
+
+ Prompt(prompt, "last_loop")
+
class SubMind:
def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: Observation):
@@ -59,7 +69,7 @@ class SubMind:
self.past_mind = []
self.structured_info = {}
- async def do_thinking_before_reply(self):
+ async def do_thinking_before_reply(self, last_cycle: CycleInfo = None):
"""
在回复前进行思考,生成内心想法并收集工具调用结果
@@ -93,7 +103,7 @@ class SubMind:
individuality = Individuality.get_instance()
# 构建个性部分
- prompt_personality = f"你的名字是{individuality.personality.bot_nickname},你"
+ prompt_personality = f"你正在扮演名为{individuality.personality.bot_nickname}的人类,你"
prompt_personality += individuality.personality.personality_core
# 随机添加个性侧面
@@ -117,12 +127,33 @@ class SubMind:
# 思考指导选项和权重
hf_options = [
- ("继续生成你在这个聊天中的想法,在原来想法的基础上继续思考,但是不要纠结于同一个话题", 0.6),
- ("生成你在这个聊天中的想法,在原来的想法上尝试新的话题", 0.1),
- ("生成你在这个聊天中的想法,不要太深入", 0.2),
- ("继续生成你在这个聊天中的想法,进行深入思考", 0.1),
+ ("可以参考之前的想法,在原来想法的基础上继续思考", 0.2),
+ ("可以参考之前的想法,在原来的想法上尝试新的话题", 0.4),
+ ("不要太深入", 0.2),
+ ("进行深入思考", 0.2),
]
+ # 上一次决策信息
+ if last_cycle != None:
+ last_action = last_cycle.action_type
+ last_reasoning = last_cycle.reasoning
+ is_replan = last_cycle.replanned
+ if is_replan:
+ if_replan_prompt = f"但是你有了上述想法之后,有了新消息,你决定重新思考后,你做了:{last_action}\n因为:{last_reasoning}\n"
+ else:
+ if_replan_prompt = f"出于这个想法,你刚才做了:{last_action}\n因为:{last_reasoning}\n"
+ else:
+ last_action = ""
+ last_reasoning = ""
+ is_replan = False
+ if_replan_prompt = ""
+ if current_thinking_info:
+ last_loop_prompt = (await global_prompt_manager.get_prompt_async("last_loop")).format(
+ current_thinking_info=current_thinking_info, if_replan_prompt=if_replan_prompt
+ )
+ else:
+ last_loop_prompt = ""
+
# 加权随机选择思考指导
hf_do_next = local_random.choices(
[option[0] for option in hf_options], weights=[option[1] for option in hf_options], k=1
@@ -134,14 +165,14 @@ class SubMind:
extra_info="", # 可以在这里添加额外信息
prompt_personality=prompt_personality,
bot_name=individuality.personality.bot_nickname,
- current_thinking_info=current_thinking_info,
time_now=time_now,
chat_observe_info=chat_observe_info,
mood_info=mood_info,
hf_do_next=hf_do_next,
+ last_loop_prompt=last_loop_prompt,
)
- logger.debug(f"[{self.subheartflow_id}] 心流思考提示词构建完成")
+ # logger.debug(f"[{self.subheartflow_id}] 心流思考提示词构建完成")
# ---------- 5. 执行LLM请求并处理响应 ----------
content = "" # 初始化内容变量
@@ -193,7 +224,8 @@ class SubMind:
content = "思考过程中出现错误"
# 记录最终思考结果
- logger.debug(f"[{self.subheartflow_id}] 心流思考结果:\n{content}\n")
+ name = chat_manager.get_stream_name(self.subheartflow_id)
+ logger.debug(f"[{name}] \nPrompt:\n{prompt}\n\n心流思考结果:\n{content}\n")
# 处理空响应情况
if not content:
diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py
index b9703e53b..6d994d769 100644
--- a/src/heart_flow/subheartflow_manager.py
+++ b/src/heart_flow/subheartflow_manager.py
@@ -2,6 +2,8 @@ import asyncio
import time
import random
from typing import Dict, Any, Optional, List
+import json # 导入 json 模块
+import functools # <-- 新增导入
# 导入日志模块
from src.common.logger import get_module_logger, LogConfig, SUBHEARTFLOW_MANAGER_STYLE_CONFIG
@@ -11,9 +13,16 @@ from src.plugins.chat.chat_stream import chat_manager
# 导入心流相关类
from src.heart_flow.sub_heartflow import SubHeartflow, ChatState
-from src.heart_flow.mai_state_manager import MaiState, MaiStateInfo
+from src.heart_flow.mai_state_manager import MaiStateInfo
from .observation import ChattingObservation
+# 导入LLM请求工具
+from src.plugins.models.utils_model import LLMRequest
+from src.config.config import global_config
+from src.individuality.individuality import Individuality
+import traceback
+
+
# 初始化日志记录器
subheartflow_manager_log_config = LogConfig(
@@ -23,41 +32,36 @@ subheartflow_manager_log_config = LogConfig(
logger = get_module_logger("subheartflow_manager", config=subheartflow_manager_log_config)
# 子心流管理相关常量
-INACTIVE_THRESHOLD_SECONDS = 1200 # 子心流不活跃超时时间(秒)
+INACTIVE_THRESHOLD_SECONDS = 3600 # 子心流不活跃超时时间(秒)
class SubHeartflowManager:
"""管理所有活跃的 SubHeartflow 实例。"""
- def __init__(self):
+ def __init__(self, mai_state_info: MaiStateInfo):
self.subheartflows: Dict[Any, "SubHeartflow"] = {}
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", # 保留特定的请求类型
+ )
def get_all_subheartflows(self) -> List["SubHeartflow"]:
"""获取所有当前管理的 SubHeartflow 实例列表 (快照)。"""
return list(self.subheartflows.values())
- def get_all_subheartflows_ids(self) -> List[Any]:
- """获取所有当前管理的 SubHeartflow ID 列表。"""
- return list(self.subheartflows.keys())
-
- def get_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:
- """获取指定 ID 的 SubHeartflow 实例。"""
- # 注意:这里没有加锁,假设读取操作相对安全或在已知上下文中调用
- # 如果并发写操作很多,get 也应该加锁
- subflow = self.subheartflows.get(subheartflow_id)
- if subflow:
- subflow.last_active_time = time.time() # 获取时更新活动时间
- return subflow
-
- async def create_or_get_subheartflow(
- self, subheartflow_id: Any, mai_states: MaiStateInfo
- ) -> Optional["SubHeartflow"]:
+ async def get_or_create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:
"""获取或创建指定ID的子心流实例
Args:
subheartflow_id: 子心流唯一标识符
- mai_states: 当前麦麦状态信息
+ # mai_states 参数已被移除,使用 self.mai_state_info
Returns:
成功返回SubHeartflow实例,失败返回None
@@ -74,11 +78,21 @@ class SubHeartflowManager:
# logger.debug(f"获取到已存在的子心流: {subheartflow_id}")
return subflow
- # 创建新的子心流实例
- logger.info(f"子心流 {subheartflow_id} 不存在,正在创建...")
try:
- # 初始化子心流
- new_subflow = SubHeartflow(subheartflow_id, mai_states)
+ # --- 使用 functools.partial 创建 HFC 回调 --- #
+ # 将 manager 的 _handle_hfc_no_reply 方法与当前的 subheartflow_id 绑定
+ hfc_callback = functools.partial(self._handle_hfc_no_reply, subheartflow_id)
+ # --- 结束创建回调 --- #
+
+ # 初始化子心流, 传入 mai_state_info 和 partial 创建的回调
+ new_subflow = SubHeartflow(
+ subheartflow_id,
+ self.mai_state_info,
+ hfc_callback, # <-- 传递 partial 创建的回调
+ )
+
+ # 异步初始化
+ await new_subflow.initialize()
# 添加聊天观察者
observation = ChattingObservation(chat_id=subheartflow_id)
@@ -86,7 +100,8 @@ class SubHeartflowManager:
# 注册子心流
self.subheartflows[subheartflow_id] = new_subflow
- logger.info(f"子心流 {subheartflow_id} 创建成功")
+ heartflow_name = chat_manager.get_stream_name(subheartflow_id) or subheartflow_id
+ logger.info(f"[{heartflow_name}] 开始接收消息")
# 启动后台任务
asyncio.create_task(new_subflow.subheartflow_start_working())
@@ -96,69 +111,77 @@ class SubHeartflowManager:
logger.error(f"创建子心流 {subheartflow_id} 失败: {e}", exc_info=True)
return None
- async def stop_subheartflow(self, subheartflow_id: Any, reason: str) -> bool:
- """停止指定的子心流并清理资源"""
- subheartflow = self.subheartflows.get(subheartflow_id)
- if not subheartflow:
- return False
+ # --- 新增:内部方法,用于尝试将单个子心流设置为 ABSENT ---
+ async def _try_set_subflow_absent_internal(self, subflow: "SubHeartflow", log_prefix: str) -> bool:
+ """
+ 尝试将给定的子心流对象状态设置为 ABSENT (内部方法,不处理锁)。
- stream_name = chat_manager.get_stream_name(subheartflow_id) or subheartflow_id
- logger.info(f"[子心流管理] 正在停止 {stream_name}, 原因: {reason}")
+ Args:
+ subflow: 子心流对象。
+ log_prefix: 用于日志记录的前缀 (例如 "[子心流管理]" 或 "[停用]")。
- try:
- # 设置状态为ABSENT释放资源
- if subheartflow.chat_state.chat_status != ChatState.ABSENT:
- logger.debug(f"[子心流管理] 设置 {stream_name} 状态为ABSENT")
- states_num = (
- self.count_subflows_by_state(ChatState.ABSENT),
- self.count_subflows_by_state(ChatState.CHAT),
- self.count_subflows_by_state(ChatState.FOCUSED),
- )
- await subheartflow.set_chat_state(ChatState.ABSENT, states_num)
- else:
- logger.debug(f"[子心流管理] {stream_name} 已是ABSENT状态")
- except Exception as e:
- logger.error(f"[子心流管理] 设置ABSENT状态失败: {e}")
+ Returns:
+ bool: 如果状态成功变为 ABSENT 或原本就是 ABSENT,返回 True;否则返回 False。
+ """
+ flow_id = subflow.subheartflow_id
+ stream_name = chat_manager.get_stream_name(flow_id) or flow_id
- # 停止子心流内部循环
- subheartflow.should_stop = True
-
- # 取消后台任务
- task = subheartflow.task
- if task and not task.done():
- task.cancel()
- logger.debug(f"[子心流管理] 已取消 {stream_name} 的后台任务")
-
- # 从管理字典中移除
- if subheartflow_id in self.subheartflows:
- del self.subheartflows[subheartflow_id]
- logger.debug(f"[子心流管理] 已移除 {stream_name}")
- return True
+ if subflow.chat_state.chat_status != ChatState.ABSENT:
+ logger.debug(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT")
+ try:
+ await subflow.change_chat_state(ChatState.ABSENT)
+ # 再次检查以确认状态已更改 (change_chat_state 内部应确保)
+ if subflow.chat_state.chat_status == ChatState.ABSENT:
+ return True
+ else:
+ logger.warning(
+ f"{log_prefix} 调用 change_chat_state 后,{stream_name} 状态仍为 {subflow.chat_state.chat_status.value}"
+ )
+ return False
+ except Exception as e:
+ logger.error(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT 时失败: {e}", exc_info=True)
+ return False
else:
- logger.warning(f"[子心流管理] {stream_name} 已被提前移除")
- return False
+ logger.debug(f"{log_prefix} {stream_name} 已是 ABSENT 状态")
+ return True # 已经是目标状态,视为成功
- def cleanup_inactive_subheartflows(self, max_age_seconds=INACTIVE_THRESHOLD_SECONDS):
- """识别并返回需要清理的不活跃子心流(id, 原因)"""
+ # --- 结束新增 ---
+
+ 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 self._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()):
- # 只检查有interest_chatting的子心流
- if hasattr(subheartflow, "interest_chatting") and subheartflow.interest_chatting:
- last_interact = subheartflow.interest_chatting.last_interaction_time
- if max_age_seconds and (current_time - last_interact) > max_age_seconds:
- reason = f"不活跃时间({current_time - last_interact:.0f}s) > 阈值({max_age_seconds}s)"
- name = chat_manager.get_stream_name(subheartflow_id) or subheartflow_id
- logger.debug(f"[清理] 标记 {name} 待移除: {reason}")
- flows_to_stop.append((subheartflow_id, reason))
+ 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
+ if max_age_seconds and (current_time - absent_last_time) > max_age_seconds:
+ flows_to_stop.append(subheartflow_id)
- if flows_to_stop:
- logger.info(f"[清理] 发现 {len(flows_to_stop)} 个不活跃子心流")
return flows_to_stop
- async def enforce_subheartflow_limits(self, current_mai_state: MaiState):
+ 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}")
@@ -181,7 +204,7 @@ class SubHeartflowManager:
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.stop_subheartflow(flow_id, f"普通聊天超额(限{normal_limit})"):
+ if await self.sleep_subheartflow(flow_id, f"普通聊天超额(限{normal_limit})"):
stopped += 1
# 处理专注聊天超额(需重新统计)
@@ -195,7 +218,7 @@ class SubHeartflowManager:
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.stop_subheartflow(flow_id, f"专注聊天超额(限{focused_limit})"):
+ if await self.sleep_subheartflow(flow_id, f"专注聊天超额(限{focused_limit})"):
stopped += 1
if stopped:
@@ -203,241 +226,301 @@ class SubHeartflowManager:
else:
logger.debug(f"[限制] 无需停止, 当前总数:{len(self.subheartflows)}")
- async def activate_random_subflows_to_chat(self, current_mai_state: MaiState):
- """主状态激活时,随机选择ABSENT子心流进入CHAT状态"""
- limit = current_mai_state.get_normal_chat_max_num()
- if limit <= 0:
- logger.info("[激活] 当前状态不允许CHAT子心流")
- return
-
- # 获取所有ABSENT状态的子心流
- absent_flows = [flow for flow in self.subheartflows.values() if flow.chat_state.chat_status == ChatState.ABSENT]
-
- num_to_activate = min(limit, len(absent_flows))
- if num_to_activate <= 0:
- logger.info(f"[激活] 无可用ABSENT子心流(限额:{limit}, 可用:{len(absent_flows)})")
- return
-
- logger.info(f"[激活] 随机选择{num_to_activate}个ABSENT子心流进入CHAT状态")
- activated_count = 0
-
- for flow in random.sample(absent_flows, num_to_activate):
- flow_id = flow.subheartflow_id
- stream_name = chat_manager.get_stream_name(flow_id) or flow_id
-
- if flow_id not in self.subheartflows:
- logger.warning(f"[激活] 跳过{stream_name}, 子心流已不存在")
- continue
-
- logger.debug(f"[激活] 正在激活子心流{stream_name}")
-
- states_num = (
- self.count_subflows_by_state(ChatState.ABSENT),
- self.count_subflows_by_state(ChatState.CHAT),
- self.count_subflows_by_state(ChatState.FOCUSED),
- )
-
- await flow.set_chat_state(ChatState.CHAT, states_num)
-
- if flow.chat_state.chat_status == ChatState.CHAT:
- activated_count += 1
- else:
- logger.warning(f"[激活] {stream_name}状态设置失败")
-
- logger.info(f"[激活] 完成, 成功激活{activated_count}个子心流")
-
async def deactivate_all_subflows(self):
- """停用所有子心流(主状态变为OFFLINE时调用)"""
- logger.info("[停用] 开始停用所有子心流")
- flow_ids = list(self.subheartflows.keys())
+ """将所有子心流的状态更改为 ABSENT (例如主状态变为OFFLINE时调用)"""
+ log_prefix = "[停用]"
+ changed_count = 0
+ processed_count = 0
- if not flow_ids:
- logger.info("[停用] 无活跃子心流")
- return
+ 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
- stopped_count = 0
- for flow_id in flow_ids:
- if await self.stop_subheartflow(flow_id, "主状态离线"):
- stopped_count += 1
+ for subflow in flows_to_update:
+ # 记录原始状态,以便统计实际改变的数量
+ original_state_was_absent = subflow.chat_state.chat_status == ChatState.ABSENT
- logger.info(f"[停用] 完成, 尝试停止{len(flow_ids)}个, 成功{stopped_count}个")
+ success = await self._try_set_subflow_absent_internal(subflow, log_prefix)
- async def evaluate_interest_and_promote(self, current_mai_state: MaiStateInfo):
+ # 如果成功设置为 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 evaluate_interest_and_promote(self):
"""评估子心流兴趣度,满足条件且未达上限则提升到FOCUSED状态(基于start_hfc_probability)"""
- log_prefix_manager = "[子心流管理器-兴趣评估]"
- logger.debug(f"{log_prefix_manager} 开始周期... 当前状态: {current_mai_state.get_current_state().value}")
-
- # 获取 FOCUSED 状态的数量上限
- current_state_enum = current_mai_state.get_current_state()
- focused_limit = current_state_enum.get_focused_chat_max_num()
- if focused_limit <= 0:
- logger.debug(
- f"{log_prefix_manager} 当前状态 ({current_state_enum.value}) 不允许 FOCUSED 子心流, 跳过提升检查。"
- )
- return
-
- # 获取当前 FOCUSED 状态的数量 (初始值)
- current_focused_count = self.count_subflows_by_state(ChatState.FOCUSED)
- logger.debug(f"{log_prefix_manager} 专注上限: {focused_limit}, 当前专注数: {current_focused_count}")
-
- # 使用快照安全遍历
- subflows_snapshot = list(self.subheartflows.values())
- promoted_count = 0 # 记录本次提升的数量
try:
- for sub_hf in subflows_snapshot:
+ log_prefix = "[兴趣评估]"
+ # 使用 self.mai_state_info 获取当前状态和限制
+ current_state = self.mai_state_info.get_current_state()
+ focused_limit = current_state.get_focused_chat_max_num()
+
+ logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 开始尝试提升到FOCUSED状态")
+
+ if int(time.time()) % 20 == 0: # 每20秒输出一次
+ logger.debug(f"{log_prefix} 当前状态 ({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"{log_prefix} 已达专注上限 ({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
- log_prefix_flow = f"[{stream_name}]"
- # 只处理 CHAT 状态的子心流
- # The code snippet is checking if the `chat_status` attribute of `sub_hf.chat_state` is not equal to
- # `ChatState.CHAT`. If the condition is met, the code will continue to the next iteration of the loop
- # or block of code where this snippet is located.
- # if sub_hf.chat_state.chat_status != ChatState.CHAT:
- # continue
+ logger.debug(f"{log_prefix} 检查子心流: {stream_name},现在状态: {sub_hf.chat_state.chat_status.value}")
- # 检查是否满足提升概率
- should_hfc = random.random() < sub_hf.interest_chatting.start_hfc_probability
- if not should_hfc:
+ # 跳过非CHAT状态或已经是FOCUSED状态的子心流
+ if sub_hf.chat_state.chat_status == ChatState.FOCUSED:
continue
- # --- 关键检查:检查 FOCUSED 数量是否已达上限 ---
- # 注意:在循环内部再次获取当前数量,因为之前的提升可能已经改变了计数
- # 使用已经记录并在循环中更新的 current_focused_count
- if current_focused_count >= focused_limit:
- logger.debug(
- f"{log_prefix_manager} {log_prefix_flow} 达到专注上限 ({current_focused_count}/{focused_limit}), 无法提升。概率={sub_hf.interest_chatting.start_hfc_probability:.2f}"
- )
- continue # 跳过这个子心流,继续检查下一个
+ 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
+
+ # 检查是否满足提升概率
+ logger.debug(
+ f"{log_prefix} 检查子心流: {stream_name},现在概率: {sub_hf.interest_chatting.start_hfc_probability}"
+ )
+ if random.random() >= sub_hf.interest_chatting.start_hfc_probability:
+ continue
+
+ # 再次检查是否达到上限
+ if current_focused_count >= focused_limit:
+ logger.debug(f"{log_prefix} [{stream_name}] 已达专注上限")
+ break
+
+ # 获取最新状态并执行提升
current_subflow = self.subheartflows.get(flow_id)
if not current_subflow:
- logger.warning(f"{log_prefix_manager} {log_prefix_flow} 尝试提升时状态已改变或实例消失,跳过。")
continue
logger.info(
- f"{log_prefix_manager} {log_prefix_flow} 兴趣评估触发升级 (prob={sub_hf.interest_chatting.start_hfc_probability:.2f}, 上限:{focused_limit}, 当前:{current_focused_count}) -> FOCUSED"
+ f"{log_prefix} [{stream_name}] 触发 认真水群 (概率={current_subflow.interest_chatting.start_hfc_probability:.2f})"
)
- states_num = (
- self.count_subflows_by_state(ChatState.ABSENT),
- self.count_subflows_by_state(ChatState.CHAT), # 这个值在提升前计算
- current_focused_count, # 这个值在提升前计算
- )
-
- # --- 状态设置 ---
- original_state = current_subflow.chat_state.chat_status # 记录原始状态
- await current_subflow.set_chat_state(ChatState.FOCUSED, states_num)
-
- # --- 状态验证 ---
- final_subflow = self.subheartflows.get(flow_id)
- if final_subflow:
- final_state = final_subflow.chat_state.chat_status
- if final_state == ChatState.FOCUSED:
- logger.debug(
- f"{log_prefix_manager} {log_prefix_flow} 成功从 {original_state.value} 升级到 FOCUSED 状态"
- )
- promoted_count += 1
- # 提升成功后,更新当前专注计数,以便后续检查能使用最新值
- current_focused_count += 1
- elif final_state == original_state: # 状态未变
- logger.warning(
- f"{log_prefix_manager} {log_prefix_flow} 尝试从 {original_state.value} 升级 FOCUSED 失败,状态仍为: {final_state.value} (可能被内部逻辑阻止)"
- )
- else: # 状态变成其他了?
- logger.warning(
- f"{log_prefix_manager} {log_prefix_flow} 尝试从 {original_state.value} 升级 FOCUSED 后状态变为 {final_state.value}"
- )
- else: # 子心流消失了?
- logger.warning(f"{log_prefix_manager} {log_prefix_flow} 升级后验证时子心流 {flow_id} 消失")
+ # 执行状态提升
+ 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"{log_prefix_manager} 兴趣评估周期出错: {e}", exc_info=True)
+ logger.error(f"启动HFC 兴趣评估失败: {e}", exc_info=True)
- if promoted_count > 0:
- logger.info(f"{log_prefix_manager} 评估周期结束, 成功提升 {promoted_count} 个子心流到 FOCUSED。")
- else:
- logger.debug(f"{log_prefix_manager} 评估周期结束, 未提升任何子心流。")
+ async def evaluate_and_transition_subflows_by_llm(self):
+ """
+ 使用LLM评估每个子心流的状态,并根据LLM的判断执行状态转换(ABSENT <-> CHAT)。
+ 注意:此函数包含对假设的LLM函数的调用。
+ """
+ # 获取当前状态和限制,用于CHAT激活检查
+ current_mai_state = self.mai_state_info.get_current_state()
+ chat_limit = current_mai_state.get_normal_chat_max_num()
- async def randomly_deactivate_subflows(self, deactivation_probability: float = 0.3):
- """以一定概率将 FOCUSED 或 CHAT 状态的子心流回退到 ABSENT 状态。"""
- log_prefix_manager = "[子心流管理器-随机停用]"
- logger.debug(f"{log_prefix_manager} 开始随机停用检查... (概率: {deactivation_probability:.0%})")
+ transitioned_to_chat = 0
+ transitioned_to_absent = 0
- # 使用快照安全遍历
- subflows_snapshot = list(self.subheartflows.values())
- deactivated_count = 0
+ async with self._lock: # 在锁内获取快照并迭代
+ subflows_snapshot = list(self.subheartflows.values())
+ # 使用不上锁的版本,因为我们已经在锁内
+ current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
- # 预先计算状态数量,因为 set_chat_state 需要
- states_num_before = (
- self.count_subflows_by_state(ChatState.ABSENT),
- self.count_subflows_by_state(ChatState.CHAT),
- self.count_subflows_by_state(ChatState.FOCUSED),
- )
+ if not subflows_snapshot:
+ logger.info("当前没有子心流需要评估。")
+ return
- try:
for sub_hf in subflows_snapshot:
flow_id = sub_hf.subheartflow_id
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
- log_prefix_flow = f"[{stream_name}]"
- current_state = sub_hf.chat_state.chat_status
+ log_prefix = f"[{stream_name}]"
+ current_subflow_state = sub_hf.chat_state.chat_status
- # 只处理 FOCUSED 或 CHAT 状态
- if current_state not in [ChatState.FOCUSED, ChatState.CHAT]:
- continue
+ _observation_summary = "没有可用的观察信息。" # 默认值
- # 检查随机概率
- if random.random() < deactivation_probability:
- logger.info(
- f"{log_prefix_manager} {log_prefix_flow} 随机触发停用 (从 {current_state.value}) -> ABSENT"
+ first_observation = sub_hf.observations[0]
+ if isinstance(first_observation, ChattingObservation):
+ # 组合中期记忆和当前聊天内容
+ await first_observation.observe()
+ current_chat = first_observation.talking_message_str or "当前无聊天内容。"
+ combined_summary = f"当前聊天内容:\n{current_chat}"
+ else:
+ logger.warning(f"{log_prefix} [{stream_name}] 第一个观察者不是 ChattingObservation 类型。")
+
+ # --- 获取麦麦状态 ---
+ mai_state_description = f"你当前状态: {current_mai_state.value}。"
+
+ # 获取个性化信息
+ individuality = Individuality.get_instance()
+
+ # 构建个性部分
+ prompt_personality = f"你正在扮演名为{individuality.personality.bot_nickname}的人类,你"
+ prompt_personality += individuality.personality.personality_core
+
+ # 随机添加个性侧面
+ if individuality.personality.personality_sides:
+ random_side = random.choice(individuality.personality.personality_sides)
+ prompt_personality += f",{random_side}"
+
+ # 随机添加身份细节
+ if individuality.identity.identity_detail:
+ random_detail = random.choice(individuality.identity.identity_detail)
+ prompt_personality += f",{random_detail}"
+
+ # --- 针对 ABSENT 状态 ---
+ if current_subflow_state == ChatState.ABSENT:
+ # 构建Prompt
+ prompt = (
+ f"{prompt_personality}\n"
+ f"你当前没有在: [{stream_name}] 群中聊天。\n"
+ f"{mai_state_description}\n"
+ f"这个群里最近的聊天内容是:\n---\n{combined_summary}\n---\n"
+ f"基于以上信息,请判断你是否愿意在这个群开始闲聊,"
+ f"进入常规聊天(CHAT)状态?\n"
+ f"给出你的判断,和理由,然后以 JSON 格式回答"
+ f"包含键 'decision',如果要开始聊天,值为 true ,否则为 false.\n"
+ f"包含键 'reason',其值为你的理由。\n"
+ f'例如:{{"decision": true, "reason": "因为我想聊天"}}\n'
+ f"请只输出有效的 JSON 对象。"
)
- # 获取当前实例以检查最新状态
- current_subflow = self.subheartflows.get(flow_id)
- if not current_subflow or current_subflow.chat_state.chat_status != current_state:
- logger.warning(f"{log_prefix_manager} {log_prefix_flow} 尝试停用时状态已改变或实例消失,跳过。")
+ # 调用LLM评估
+ should_activate = await self._llm_evaluate_state_transition(prompt)
+ if should_activate is None: # 处理解析失败或意外情况
+ logger.warning(f"{log_prefix}LLM评估返回无效结果,跳过。")
continue
- # --- 状态设置 --- #
- # 注意:这里传递的状态数量是 *停用前* 的状态数量
- await current_subflow.set_chat_state(ChatState.ABSENT, states_num_before)
-
- # --- 状态验证 (可选) ---
- final_subflow = self.subheartflows.get(flow_id)
- if final_subflow:
- final_state = final_subflow.chat_state.chat_status
- if final_state == ChatState.ABSENT:
- logger.debug(
- f"{log_prefix_manager} {log_prefix_flow} 成功从 {current_state.value} 停用到 ABSENT 状态"
+ if should_activate:
+ # 检查CHAT限额
+ # 使用不上锁的版本,因为我们已经在锁内
+ current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
+ if current_chat_count < chat_limit:
+ logger.info(
+ f"{log_prefix}LLM建议激活到CHAT状态,且未达上限({current_chat_count}/{chat_limit})。正在尝试转换..."
)
- deactivated_count += 1
- # 注意:停用后不需要更新 states_num_before,因为它只用于 set_chat_state 的限制检查
+ await sub_hf.change_chat_state(ChatState.CHAT)
+ if sub_hf.chat_state.chat_status == ChatState.CHAT:
+ transitioned_to_chat += 1
+ else:
+ logger.warning(f"{log_prefix}尝试激活到CHAT失败。")
else:
- logger.warning(
- f"{log_prefix_manager} {log_prefix_flow} 尝试停用到 ABSENT 后状态仍为 {final_state.value}"
+ logger.info(
+ f"{log_prefix}LLM建议激活到CHAT状态,但已达到上限({current_chat_count}/{chat_limit})。跳过转换。"
)
else:
- logger.warning(f"{log_prefix_manager} {log_prefix_flow} 停用后验证时子心流 {flow_id} 消失")
+ logger.info(f"{log_prefix}LLM建议不激活到CHAT状态。")
- except Exception as e:
- logger.error(f"{log_prefix_manager} 随机停用周期出错: {e}", exc_info=True)
+ # --- 针对 CHAT 状态 ---
+ elif current_subflow_state == ChatState.CHAT:
+ # 构建Prompt
+ prompt = (
+ f"{prompt_personality}\n"
+ f"你正在在: [{stream_name}] 群中聊天。\n"
+ f"{mai_state_description}\n"
+ f"这个群里最近的聊天内容是:\n---\n{combined_summary}\n---\n"
+ f"基于以上信息,请判断你是否愿意在这个群继续闲聊,"
+ f"还是暂时离开聊天,进入休眠状态?\n"
+ f"给出你的判断,和理由,然后以 JSON 格式回答"
+ f"包含键 'decision',如果要离开聊天,值为 true ,否则为 false.\n"
+ f"包含键 'reason',其值为你的理由。\n"
+ f'例如:{{"decision": true, "reason": "因为我想休息"}}\n'
+ f"请只输出有效的 JSON 对象。"
+ )
- if deactivated_count > 0:
- logger.info(f"{log_prefix_manager} 随机停用周期结束, 成功停用 {deactivated_count} 个子心流。")
- else:
- logger.debug(f"{log_prefix_manager} 随机停用周期结束, 未停用任何子心流。")
+ # 调用LLM评估
+ should_deactivate = await self._llm_evaluate_state_transition(prompt)
+ if should_deactivate is None: # 处理解析失败或意外情况
+ logger.warning(f"{log_prefix}LLM评估返回无效结果,跳过。")
+ continue
- def count_subflows_by_state(self, state: ChatState) -> int:
- """统计指定状态的子心流数量
+ if should_deactivate:
+ logger.info(f"{log_prefix}LLM建议进入ABSENT状态。正在尝试转换...")
+ await sub_hf.change_chat_state(ChatState.ABSENT)
+ if sub_hf.chat_state.chat_status == ChatState.ABSENT:
+ transitioned_to_absent += 1
+ else:
+ logger.info(f"{log_prefix}LLM建议不进入ABSENT状态。")
+
+ async def _llm_evaluate_state_transition(self, prompt: str) -> Optional[bool]:
+ """
+ 使用 LLM 评估是否应进行状态转换,期望 LLM 返回 JSON 格式。
Args:
- state: 要统计的聊天状态枚举值
+ prompt: 提供给 LLM 的提示信息,要求返回 {"decision": true/false}。
Returns:
- int: 处于该状态的子心流数量
+ 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
+
+ if isinstance(decision, bool):
+ logger.debug(f"{log_prefix} LLM评估结果 (来自JSON): {'建议转换' if decision else '建议不转换'}")
+ return decision
+ else:
+ logger.warning(
+ f"{log_prefix} LLM 返回的 JSON 中 'decision' 键的值不是布尔型: {decision}。响应: {response_text}"
+ )
+ return 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
+ if "false" in response_text.lower():
+ logger.debug(f"{log_prefix} 在非JSON响应中找到 'false',解释为建议不转换")
+ return False
+ return None # JSON 解析失败,也未找到关键词
+ except Exception as parse_err: # 捕获其他可能的解析错误
+ logger.warning(f"{log_prefix} 解析 LLM JSON 响应时发生意外错误: {parse_err}。响应: {response_text}")
+ return None
+
+ except Exception as e:
+ logger.error(f"{log_prefix} 调用 LLM 或处理其响应时出错: {e}", exc_info=True)
+ traceback.print_exc()
+ return None # LLM 调用或处理失败
+
+ def count_subflows_by_state(self, state: ChatState) -> int:
+ """统计指定状态的子心流数量"""
count = 0
# 遍历所有子心流实例
for subheartflow in self.subheartflows.values():
@@ -446,12 +529,19 @@ class SubHeartflowManager:
count += 1
return count
- def get_active_subflow_minds(self) -> List[str]:
- """获取所有活跃(非ABSENT)子心流的当前想法
-
- 返回:
- List[str]: 包含所有活跃子心流当前想法的列表
+ def count_subflows_by_state_nolock(self, state: ChatState) -> int:
"""
+ 统计指定状态的子心流数量 (不上锁版本)。
+ 警告:仅应在已持有 self._lock 的上下文中使用此方法。
+ """
+ count = 0
+ for subheartflow in self.subheartflows.values():
+ if subheartflow.chat_state.chat_status == state:
+ count += 1
+ return count
+
+ def get_active_subflow_minds(self) -> List[str]:
+ """获取所有活跃(非ABSENT)子心流的当前想法"""
minds = []
for subheartflow in self.subheartflows.values():
# 检查子心流是否活跃(非ABSENT状态)
@@ -468,44 +558,62 @@ class SubHeartflowManager:
)
logger.debug(f"[子心流管理器] 更新了{updated_count}个子心流的主想法")
- async def deactivate_subflow(self, subheartflow_id: Any):
- """停用并移除指定的子心流。"""
+ async def delete_subflow(self, subheartflow_id: Any):
+ """删除指定的子心流。"""
async with self._lock:
subflow = self.subheartflows.pop(subheartflow_id, None)
if subflow:
- logger.info(f"正在停用 SubHeartflow: {subheartflow_id}...")
+ logger.info(f"正在删除 SubHeartflow: {subheartflow_id}...")
try:
- # --- 调用 shutdown 方法 ---
+ # 调用 shutdown 方法确保资源释放
await subflow.shutdown()
- # --- 结束调用 ---
- logger.info(f"SubHeartflow {subheartflow_id} 已成功停用。")
+ logger.info(f"SubHeartflow {subheartflow_id} 已成功删除。")
except Exception as e:
- logger.error(f"停用 SubHeartflow {subheartflow_id} 时出错: {e}", exc_info=True)
+ logger.error(f"删除 SubHeartflow {subheartflow_id} 时出错: {e}", exc_info=True)
else:
- logger.warning(f"尝试停用不存在的 SubHeartflow: {subheartflow_id}")
+ logger.warning(f"尝试删除不存在的 SubHeartflow: {subheartflow_id}")
- async def cleanup_inactive_subflows(self, inactive_threshold_seconds: int):
- """清理长时间不活跃的子心流。"""
- current_time = time.time()
- inactive_ids = []
- # 不加锁地迭代,识别不活跃的 ID
- for sub_id, subflow in self.subheartflows.items():
- # 检查 last_active_time 是否存在且是数值
- last_active = getattr(subflow, "last_active_time", 0)
- if isinstance(last_active, (int, float)):
- if current_time - last_active > inactive_threshold_seconds:
- inactive_ids.append(sub_id)
- logger.info(
- f"发现不活跃的 SubHeartflow: {sub_id} (上次活跃: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(last_active))})"
- )
+ # --- 新增:处理 HFC 无回复回调的专用方法 --- #
+ async def _handle_hfc_no_reply(self, subheartflow_id: Any):
+ """处理来自 HeartFChatting 的连续无回复信号 (通过 partial 绑定 ID)"""
+ # 注意:这里不需要再获取锁,因为 request_absent_transition 内部会处理锁
+ logger.debug(f"[管理器 HFC 处理器] 接收到来自 {subheartflow_id} 的 HFC 无回复信号")
+ await self.request_absent_transition(subheartflow_id)
+
+ # --- 结束新增 --- #
+
+ # --- 新增:处理来自 HeartFChatting 的状态转换请求 --- #
+ async def request_absent_transition(self, subflow_id: Any):
+ """
+ 接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT。
+ 通常在连续多次 "no_reply" 后被调用。
+
+ Args:
+ subflow_id: 需要转换状态的子心流 ID。
+ """
+ async with self._lock:
+ subflow = self.subheartflows.get(subflow_id)
+ if not subflow:
+ logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 ABSENT")
+ return
+
+ stream_name = chat_manager.get_stream_name(subflow_id) or subflow_id
+ current_state = subflow.chat_state.chat_status
+
+ # 仅当子心流处于 FOCUSED 状态时才进行转换
+ # 因为 HeartFChatting 只在 FOCUSED 状态下运行
+ if current_state == ChatState.FOCUSED:
+ logger.info(f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 转换为 ABSENT")
+ try:
+ await subflow.change_chat_state(ChatState.ABSENT)
+ logger.info(f"[状态转换请求] {stream_name} 状态已成功转换为 ABSENT")
+ except Exception as e:
+ logger.error(f"[状态转换请求] 转换 {stream_name} 到 ABSENT 时出错: {e}", exc_info=True)
+ elif current_state == ChatState.ABSENT:
+ logger.debug(f"[状态转换请求] {stream_name} 已处于 ABSENT 状态,无需转换")
else:
- logger.warning(f"SubHeartflow {sub_id} 的 last_active_time 无效: {last_active}。跳过清理检查。")
+ logger.warning(
+ f"[状态转换请求] 收到对 {stream_name} 的请求,但其状态为 {current_state.value} (非 FOCUSED),不执行转换"
+ )
- if inactive_ids:
- logger.info(f"准备清理 {len(inactive_ids)} 个不活跃的 SubHeartflows: {inactive_ids}")
- # 逐个停用(deactivate_subflow 会加锁)
- tasks = [self.deactivate_subflow(sub_id) for sub_id in inactive_ids]
- await asyncio.gather(*tasks)
- logger.info("不活跃的 SubHeartflows 清理完成。")
- # else:
- # logger.debug("没有发现不活跃的 SubHeartflows 需要清理。")
+ # --- 结束新增 --- #
diff --git a/src/main.py b/src/main.py
index 62fa70a6e..a2d8fc512 100644
--- a/src/main.py
+++ b/src/main.py
@@ -3,7 +3,7 @@ import time
from .plugins.utils.statistic import LLMStatistics
from .plugins.moods.moods import MoodManager
from .plugins.schedule.schedule_generator import bot_schedule
-from .plugins.chat.emoji_manager import emoji_manager
+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
from .plugins.chat.chat_stream import chat_manager
@@ -128,7 +128,6 @@ class MainSystem:
self.print_mood_task(),
self.remove_recalled_message_task(),
emoji_manager.start_periodic_check_register(),
- # emoji_manager.start_periodic_register(),
self.app.run(),
self.server.run(),
]
@@ -155,7 +154,7 @@ class MainSystem:
"""打印情绪状态"""
while True:
self.mood_manager.print_mood_status()
- await asyncio.sleep(30)
+ await asyncio.sleep(60)
@staticmethod
async def remove_recalled_message_task():
diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py
index 55bf067ef..6b8ee914b 100644
--- a/src/plugins/PFC/action_planner.py
+++ b/src/plugins/PFC/action_planner.py
@@ -8,6 +8,7 @@ from .pfc_utils import get_items_from_json
from src.individuality.individuality import Individuality
from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo
+from src.plugins.utils.chat_message_builder import build_readable_messages
pfc_action_log_config = LogConfig(
console_format=PFC_ACTION_PLANNER_STYLE_CONFIG["console_format"],
@@ -132,12 +133,7 @@ class ActionPlanner:
chat_history_text = ""
try:
if hasattr(observation_info, "chat_history") and observation_info.chat_history:
- chat_history_list = observation_info.chat_history[-20:]
- for msg in chat_history_list:
- if isinstance(msg, dict) and "detailed_plain_text" in msg:
- chat_history_text += f"{msg.get('detailed_plain_text', '')}\n"
- elif isinstance(msg, str):
- chat_history_text += f"{msg}\n"
+ chat_history_text = observation_info.chat_history_str
if not chat_history_text: # 如果历史记录是空列表
chat_history_text = "还没有聊天记录。\n"
else:
@@ -146,12 +142,16 @@ class ActionPlanner:
if hasattr(observation_info, "new_messages_count") and observation_info.new_messages_count > 0:
if hasattr(observation_info, "unprocessed_messages") and observation_info.unprocessed_messages:
new_messages_list = observation_info.unprocessed_messages
- chat_history_text += f"--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n"
- for msg in new_messages_list:
- if isinstance(msg, dict) and "detailed_plain_text" in msg:
- chat_history_text += f"{msg.get('detailed_plain_text', '')}\n"
- elif isinstance(msg, str):
- chat_history_text += f"{msg}\n"
+ new_messages_str = await build_readable_messages(
+ new_messages_list,
+ replace_bot_name=True,
+ merge_messages=False,
+ timestamp_mode="relative",
+ read_mark=0.0,
+ )
+ chat_history_text += (
+ f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}"
+ )
# 清理消息应该由调用者或 observation_info 内部逻辑处理,这里不再调用 clear
# if hasattr(observation_info, 'clear_unprocessed_messages'):
# observation_info.clear_unprocessed_messages()
diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py
index 60acb5f53..697833c84 100644
--- a/src/plugins/PFC/chat_observer.py
+++ b/src/plugins/PFC/chat_observer.py
@@ -3,7 +3,7 @@ import asyncio
import traceback
from typing import Optional, Dict, Any, List
from src.common.logger import get_module_logger
-from ..message.message_base import UserInfo
+from maim_message import UserInfo
from ...config.config import global_config
from .chat_states import NotificationManager, create_new_message_notification, create_cold_chat_notification
from .message_storage import MongoDBMessageStorage
diff --git a/src/plugins/PFC/chat_states.py b/src/plugins/PFC/chat_states.py
index dc7b728b0..1f8ee15fb 100644
--- a/src/plugins/PFC/chat_states.py
+++ b/src/plugins/PFC/chat_states.py
@@ -98,7 +98,7 @@ class NotificationManager:
notification_type: 要处理的通知类型
handler: 处理器实例
"""
- print(1145145511114445551111444)
+ # print(1145145511114445551111444)
if target not in self._handlers:
# print("没11有target")
self._handlers[target] = {}
@@ -146,9 +146,9 @@ class NotificationManager:
if target in self._handlers:
handlers = self._handlers[target].get(notification.type, [])
# print(1111111)
- print(handlers)
+ # print(handlers)
for handler in handlers:
- print(f"调用处理器: {handler}")
+ # print(f"调用处理器: {handler}")
await handler.handle_notification(notification)
def get_active_states(self) -> Set[NotificationType]:
diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py
index 9205a7c65..1b25da30c 100644
--- a/src/plugins/PFC/conversation.py
+++ b/src/plugins/PFC/conversation.py
@@ -2,7 +2,7 @@ import time
import asyncio
import datetime
# from .message_storage import MongoDBMessageStorage
-from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat
+from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_ch
from ...config.config import global_config
from typing import Dict, Any, Optional
from ..chat.message import Message
@@ -14,7 +14,7 @@ from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo
from .reply_generator import ReplyGenerator
from ..chat.chat_stream import ChatStream
-from ..message.message_base import UserInfo
+from maim_message import UserInfo
from src.plugins.chat.chat_stream import chat_manager
from .pfc_KnowledgeFetcher import KnowledgeFetcher
from .waiter import Waiter
@@ -77,14 +77,22 @@ class Conversation:
raise
try:
logger.info(f"为 {self.stream_id} 加载初始聊天记录...")
- initial_messages = await get_raw_msg_before_timestamp_with_chat( #
+ initial_messages = get_raw_msg_before_timestamp_with_chat( #
chat_id=self.stream_id,
timestamp=time.time(),
limit=30, # 加载最近30条作为初始上下文,可以调整
)
+ chat_talking_prompt = await build_readable_messages(
+ initial_messages,
+ replace_bot_name=True,
+ merge_messages=False,
+ timestamp_mode="relative",
+ read_mark=0.0,
+ )
if initial_messages:
# 将加载的消息填充到 ObservationInfo 的 chat_history
self.observation_info.chat_history = initial_messages
+ self.observation_info.chat_history_str = chat_talking_prompt + "\n"
self.observation_info.chat_history_count = len(initial_messages)
# 更新 ObservationInfo 中的时间戳等信息
@@ -174,7 +182,7 @@ class Conversation:
if hasattr(self.observation_info, "clear_unprocessed_messages"):
# 确保 clear_unprocessed_messages 方法存在
logger.debug(f"准备执行 direct_reply,清理 {initial_new_message_count} 条规划时已知的新消息。")
- self.observation_info.clear_unprocessed_messages()
+ await self.observation_info.clear_unprocessed_messages()
# 手动重置计数器,确保状态一致性(理想情况下 clear 方法会做这个)
if hasattr(self.observation_info, "new_messages_count"):
self.observation_info.new_messages_count = 0
@@ -259,7 +267,7 @@ class Conversation:
# --- 根据不同的 action 执行 ---
if action == "direct_reply":
- max_reply_attempts = 3 # 设置最大尝试次数(与 reply_checker.py 中的 max_retries 保持一致或稍大)
+ max_reply_attempts = 3 # 设置最大尝试次数(与 reply_checker.py 中的 max_retries 保持一致或稍大)
reply_attempt_count = 0
is_suitable = False
need_replan = False
@@ -284,17 +292,20 @@ class Conversation:
reply=self.generated_reply,
goal=current_goal_str,
chat_history=observation_info.chat_history,
- retry_count=reply_attempt_count - 1, # 传递当前尝试次数(从0开始计数)
+ chat_history_str=observation_info.chat_history_str,
+ retry_count=reply_attempt_count - 1, # 传递当前尝试次数(从0开始计数)
+ )
+ logger.info(
+ f"第 {reply_attempt_count} 次检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}"
)
- logger.info(f"第 {reply_attempt_count} 次检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}")
if is_suitable:
- final_reply_to_send = self.generated_reply # 保存合适的回复
- break # 回复合适,跳出循环
+ final_reply_to_send = self.generated_reply # 保存合适的回复
+ break # 回复合适,跳出循环
elif need_replan:
- logger.warning(f"第 {reply_attempt_count} 次检查建议重新规划,停止尝试。原因: {check_reason}")
- break # 如果检查器建议重新规划,也停止尝试
+ logger.warning(f"第 {reply_attempt_count} 次检查建议重新规划,停止尝试。原因: {check_reason}")
+ break # 如果检查器建议重新规划,也停止尝试
# 如果不合适但不需要重新规划,循环会继续进行下一次尝试
except Exception as check_err:
@@ -321,7 +332,7 @@ class Conversation:
return
# 发送合适的回复
- self.generated_reply = final_reply_to_send # 确保 self.generated_reply 是最终要发送的内容
+ self.generated_reply = final_reply_to_send # 确保 self.generated_reply 是最终要发送的内容
await self._send_reply()
# 更新 action 历史状态为 done
@@ -337,7 +348,7 @@ class Conversation:
logger.warning(f"经过 {reply_attempt_count} 次尝试,未能生成合适的回复。最终原因: {check_reason}")
conversation_info.done_action[action_index].update(
{
- "status": "recall", # 标记为 recall 因为没有成功发送
+ "status": "recall", # 标记为 recall 因为没有成功发送
"final_reason": f"尝试{reply_attempt_count}次后失败: {check_reason}",
"time": datetime.datetime.now().strftime("%H:%M:%S"),
}
@@ -352,7 +363,7 @@ class Conversation:
wait_action_record = {
"action": "wait",
"plan_reason": "因 direct_reply 多次尝试失败而执行的后备等待",
- "status": "done", # wait 完成后可以认为是 done
+ "status": "done", # wait 完成后可以认为是 done
"time": datetime.datetime.now().strftime("%H:%M:%S"),
"final_reason": None,
}
@@ -461,42 +472,11 @@ class Conversation:
try:
# 外层 try: 捕获发送消息和后续处理中的主要错误
- current_time = time.time() # 获取当前时间戳
+ _current_time = time.time() # 获取当前时间戳
reply_content = self.generated_reply # 获取要发送的内容
# 发送消息
await self.direct_sender.send_message(chat_stream=self.chat_stream, content=reply_content)
- logger.info(f"消息已发送: {reply_content}") # 可以在发送后加个日志确认
-
- # --- 添加的立即更新状态逻辑开始 ---
- try:
- # 内层 try: 专门捕获手动更新状态时可能出现的错误
- # 创建一个代表刚刚发送的消息的字典
- bot_message_info = {
- "message_id": f"bot_sent_{current_time}", # 创建一个简单的唯一ID
- "time": current_time,
- "user_info": UserInfo( # 使用 UserInfo 类构建用户信息
- user_id=str(global_config.BOT_QQ),
- user_nickname=global_config.BOT_NICKNAME,
- platform=self.chat_stream.platform, # 从 chat_stream 获取平台信息
- ).to_dict(), # 转换为字典格式存储
- "processed_plain_text": reply_content, # 使用发送的内容
- "detailed_plain_text": f"{int(current_time)},{global_config.BOT_NICKNAME}:{reply_content}", # 构造一个简单的详细文本, 时间戳取整
- # 可以根据需要添加其他字段,保持与 observation_info.chat_history 中其他消息结构一致
- }
-
- # 直接更新 ObservationInfo 实例
- if self.observation_info:
- self.observation_info.chat_history.append(bot_message_info) # 将消息添加到历史记录末尾
- self.observation_info.last_bot_speak_time = current_time # 更新 Bot 最后发言时间
- self.observation_info.last_message_time = current_time # 更新最后消息时间
- logger.debug("已手动将Bot发送的消息添加到 ObservationInfo")
- else:
- logger.warning("无法手动更新 ObservationInfo:实例不存在")
-
- except Exception as update_err:
- logger.error(f"手动更新 ObservationInfo 时出错: {update_err}")
- # --- 添加的立即更新状态逻辑结束 ---
# 原有的触发更新和等待代码
self.chat_observer.trigger_update()
diff --git a/src/plugins/PFC/message_sender.py b/src/plugins/PFC/message_sender.py
index bc4499ed9..8a0f41762 100644
--- a/src/plugins/PFC/message_sender.py
+++ b/src/plugins/PFC/message_sender.py
@@ -2,7 +2,7 @@ from typing import Optional
from src.common.logger import get_module_logger
from ..chat.chat_stream import ChatStream
from ..chat.message import Message
-from ..message.message_base import Seg
+from maim_message import Seg
from src.plugins.chat.message import MessageSending, MessageSet
from src.plugins.chat.message_sender import message_manager
diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py
index 08ff3c046..072b1fb6f 100644
--- a/src/plugins/PFC/observation_info.py
+++ b/src/plugins/PFC/observation_info.py
@@ -1,12 +1,13 @@
# Programmable Friendly Conversationalist
# Prefrontal cortex
from typing import List, Optional, Dict, Any, Set
-from ..message.message_base import UserInfo
+from maim_message import UserInfo
import time
from dataclasses import dataclass, field
from src.common.logger import get_module_logger
from .chat_observer import ChatObserver
from .chat_states import NotificationHandler, NotificationType
+from src.plugins.utils.chat_message_builder import build_readable_messages
logger = get_module_logger("observation_info")
@@ -97,6 +98,7 @@ class ObservationInfo:
# data_list
chat_history: List[str] = field(default_factory=list)
+ chat_history_str: str = ""
unprocessed_messages: List[Dict[str, Any]] = field(default_factory=list)
active_users: Set[str] = field(default_factory=set)
@@ -223,11 +225,18 @@ class ObservationInfo:
return None
return time.time() - self.last_bot_speak_time
- def clear_unprocessed_messages(self):
+ async def clear_unprocessed_messages(self):
"""清空未处理消息列表"""
# 将未处理消息添加到历史记录中
for message in self.unprocessed_messages:
self.chat_history.append(message)
+ self.chat_history_str = await build_readable_messages(
+ self.chat_history[-20:] if len(self.chat_history) > 20 else self.chat_history,
+ replace_bot_name=True,
+ merge_messages=False,
+ timestamp_mode="relative",
+ read_mark=0.0,
+ )
# 清空未处理消息列表
self.has_unread_messages = False
self.unprocessed_messages.clear()
diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py
index 873d14674..5a70d02f3 100644
--- a/src/plugins/PFC/pfc.py
+++ b/src/plugins/PFC/pfc.py
@@ -6,7 +6,7 @@ import datetime
from typing import List, Optional, Tuple, TYPE_CHECKING
from src.common.logger import get_module_logger
from ..chat.chat_stream import ChatStream
-from ..message.message_base import UserInfo, Seg
+from maim_message import UserInfo, Seg
from ..chat.message import Message
from ..models.utils_model import LLMRequest
from ...config.config import global_config
@@ -19,6 +19,7 @@ from src.individuality.individuality import Individuality
from .conversation_info import ConversationInfo
from .observation_info import ObservationInfo
import time
+from src.plugins.utils.chat_message_builder import build_readable_messages
if TYPE_CHECKING:
pass
@@ -80,19 +81,20 @@ class GoalAnalyzer:
goals_str = f"目标:{goal},产生该对话目标的原因:{reasoning}\n"
# 获取聊天历史记录
- chat_history_list = observation_info.chat_history
- chat_history_text = ""
- for msg in chat_history_list:
- chat_history_text += f"{msg}\n"
+ chat_history_text = observation_info.chat_history
if observation_info.new_messages_count > 0:
new_messages_list = observation_info.unprocessed_messages
+ new_messages_str = await build_readable_messages(
+ new_messages_list,
+ replace_bot_name=True,
+ merge_messages=False,
+ timestamp_mode="relative",
+ read_mark=0.0,
+ )
+ chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}"
- chat_history_text += f"有{observation_info.new_messages_count}条新消息:\n"
- for msg in new_messages_list:
- chat_history_text += f"{msg}\n"
-
- observation_info.clear_unprocessed_messages()
+ # await observation_info.clear_unprocessed_messages()
identity_details_only = self.identity_detail_info
identity_addon = ""
@@ -371,22 +373,12 @@ class DirectMessageSender:
# 处理消息
await message.process()
- message_json = message.to_dict()
+ _message_json = message.to_dict()
# 发送消息
try:
- end_point = global_config.api_urls.get(message.message_info.platform, None)
- if end_point:
- # logger.info(f"发送消息到{end_point}")
- # logger.info(message_json)
- try:
- await global_api.send_message_REST(end_point, message_json)
- except Exception as e:
- logger.error(f"REST方式发送失败,出现错误: {str(e)}")
- logger.info("尝试使用ws发送")
- await self.send_via_ws(message)
- else:
- await self.send_via_ws(message)
+ await self.send_via_ws(message)
+ await self.storage.store_message(message, chat_stream)
logger.success(f"PFC消息已发送: {content}")
except Exception as e:
logger.error(f"PFC消息发送失败: {str(e)}")
diff --git a/src/plugins/PFC/pfc_KnowledgeFetcher.py b/src/plugins/PFC/pfc_KnowledgeFetcher.py
index 1a0d495c3..95e66c8cd 100644
--- a/src/plugins/PFC/pfc_KnowledgeFetcher.py
+++ b/src/plugins/PFC/pfc_KnowledgeFetcher.py
@@ -4,6 +4,7 @@ from src.plugins.memory_system.Hippocampus import HippocampusManager
from ..models.utils_model import LLMRequest
from ...config.config import global_config
from ..chat.message import Message
+from ..knowledge.knowledge_lib import qa_manager
logger = get_module_logger("knowledge_fetcher")
@@ -19,6 +20,25 @@ class KnowledgeFetcher:
request_type="knowledge_fetch",
)
+ def _lpmm_get_knowledge(self, query: str) -> str:
+ """获取相关知识
+
+ Args:
+ query: 查询内容
+
+ Returns:
+ str: 构造好的,带相关度的知识
+ """
+
+ logger.debug("正在从LPMM知识库中获取知识")
+ try:
+ knowledge_info = qa_manager.get_knowledge(query)
+ logger.debug(f"LPMM知识库查询结果: {knowledge_info:150}")
+ return knowledge_info
+ except Exception as e:
+ logger.error(f"LPMM知识库搜索工具执行失败: {str(e)}")
+ return "未找到匹配的知识"
+
async def fetch(self, query: str, chat_history: List[Message]) -> Tuple[str, str]:
"""获取相关知识
@@ -43,13 +63,16 @@ class KnowledgeFetcher:
max_depth=3,
fast_retrieval=False,
)
-
+ knowledge = ""
if related_memory:
- knowledge = ""
sources = []
for memory in related_memory:
knowledge += memory[1] + "\n"
sources.append(f"记忆片段{memory[0]}")
- return knowledge.strip(), ",".join(sources)
+ knowledge = knowledge.strip(), ",".join(sources)
+
+ knowledge += "现在有以下**知识**可供参考:\n "
+ knowledge += self._lpmm_get_knowledge(query)
+ knowledge += "请记住这些**知识**,并根据**知识**回答问题。\n"
return "未找到相关知识", "无记忆匹配"
diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py
index 3ce284e4e..26b20875c 100644
--- a/src/plugins/PFC/reply_checker.py
+++ b/src/plugins/PFC/reply_checker.py
@@ -1,11 +1,10 @@
import json
-import datetime
from typing import Tuple, List, Dict, Any
from src.common.logger import get_module_logger
from ..models.utils_model import LLMRequest
from ...config.config import global_config
from .chat_observer import ChatObserver
-from ..message.message_base import UserInfo
+from maim_message import UserInfo
logger = get_module_logger("reply_checker")
@@ -22,7 +21,7 @@ class ReplyChecker:
self.max_retries = 3 # 最大重试次数
async def check(
- self, reply: str, goal: str, chat_history: List[Dict[str, Any]], retry_count: int = 0
+ self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_text: str, retry_count: int = 0
) -> Tuple[bool, str, bool]:
"""检查生成的回复是否合适
@@ -36,7 +35,6 @@ class ReplyChecker:
"""
# 不再从 observer 获取,直接使用传入的 chat_history
# messages = self.chat_observer.get_cached_messages(limit=20)
- chat_history_text = ""
try:
# 筛选出最近由 Bot 自己发送的消息
bot_messages = []
@@ -78,16 +76,9 @@ class ReplyChecker:
except Exception as e:
import traceback
- logger.error(f"检查回复时出错: 类型={type(e)}, 值={e}")
- logger.error(traceback.format_exc()) # 打印详细的回溯信息
- for msg in chat_history[-20:]:
- time_str = datetime.datetime.fromtimestamp(msg["time"]).strftime("%H:%M:%S")
- user_info = UserInfo.from_dict(msg.get("user_info", {}))
- sender = user_info.user_nickname or f"用户{user_info.user_id}"
- if sender == self.name:
- sender = "你说"
- chat_history_text += f"{time_str},{sender}:{msg.get('processed_plain_text', '')}\n"
+ logger.error(f"检查回复时出错: 类型={type(e)}, 值={e}")
+ logger.error(traceback.format_exc()) # 打印详细的回溯信息
prompt = f"""请检查以下回复或消息是否合适:
diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py
index fe9dab6f5..65afbf64d 100644
--- a/src/plugins/PFC/reply_generator.py
+++ b/src/plugins/PFC/reply_generator.py
@@ -7,6 +7,7 @@ from .reply_checker import ReplyChecker
from src.individuality.individuality import Individuality
from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo
+from src.plugins.utils.chat_message_builder import build_readable_messages
logger = get_module_logger("reply_generator")
@@ -68,23 +69,19 @@ class ReplyGenerator:
goals_str = f"目标:{goal},产生该对话目标的原因:{reasoning}\n"
# 获取聊天历史记录
- chat_history_list = (
- observation_info.chat_history[-20:]
- if len(observation_info.chat_history) >= 20
- else observation_info.chat_history
- )
- chat_history_text = ""
- for msg in chat_history_list:
- chat_history_text += f"{msg.get('detailed_plain_text', '')}\n"
+ chat_history_text = observation_info.chat_history_str
if observation_info.new_messages_count > 0:
new_messages_list = observation_info.unprocessed_messages
-
- chat_history_text += f"有{observation_info.new_messages_count}条新消息:\n"
- for msg in new_messages_list:
- chat_history_text += f"{msg.get('detailed_plain_text', '')}\n"
-
- observation_info.clear_unprocessed_messages()
+ new_messages_str = await build_readable_messages(
+ new_messages_list,
+ replace_bot_name=True,
+ merge_messages=False,
+ timestamp_mode="relative",
+ read_mark=0.0,
+ )
+ chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}"
+ # await observation_info.clear_unprocessed_messages()
identity_details_only = self.identity_detail_info
identity_addon = ""
@@ -173,7 +170,7 @@ class ReplyGenerator:
return "抱歉,我现在有点混乱,让我重新思考一下..."
async def check_reply(
- self, reply: str, goal: str, chat_history: List[Dict[str, Any]], retry_count: int = 0
+ self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_str: str, retry_count: int = 0
) -> Tuple[bool, str, bool]:
"""检查回复是否合适
@@ -185,4 +182,4 @@ class ReplyGenerator:
Returns:
Tuple[bool, str, bool]: (是否合适, 原因, 是否需要重新规划)
"""
- return await self.reply_checker.check(reply, goal, chat_history, retry_count)
+ return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, retry_count)
diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py
index 85de966e0..2e057e6fe 100644
--- a/src/plugins/__init__.py
+++ b/src/plugins/__init__.py
@@ -4,7 +4,7 @@ MaiMBot插件系统
"""
from .chat.chat_stream import chat_manager
-from .chat.emoji_manager import emoji_manager
+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
diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py
index 8d9aa1f8e..e5b0b942b 100644
--- a/src/plugins/chat/__init__.py
+++ b/src/plugins/chat/__init__.py
@@ -1,4 +1,4 @@
-from .emoji_manager import emoji_manager
+from ..emoji_system.emoji_manager import emoji_manager
from ..person_info.relationship_manager import relationship_manager
from .chat_stream import chat_manager
from .message_sender import message_manager
diff --git a/src/plugins/chat/chat_stream.py b/src/plugins/chat/chat_stream.py
index e50dc3ec2..9416ebadf 100644
--- a/src/plugins/chat/chat_stream.py
+++ b/src/plugins/chat/chat_stream.py
@@ -6,7 +6,7 @@ from typing import Dict, Optional
from ...common.database import db
-from ..message.message_base import GroupInfo, UserInfo
+from maim_message import GroupInfo, UserInfo
from src.common.logger import get_module_logger, LogConfig, CHAT_STREAM_STYLE_CONFIG
diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py
deleted file mode 100644
index cbc8e600a..000000000
--- a/src/plugins/chat/emoji_manager.py
+++ /dev/null
@@ -1,595 +0,0 @@
-import asyncio
-import base64
-import hashlib
-import os
-import random
-import time
-import traceback
-from typing import Optional, Tuple
-from PIL import Image
-import io
-
-from ...common.database import db
-from ...config.config import global_config
-from ..chat.utils import get_embedding
-from ..chat.utils_image import ImageManager, image_path_to_base64
-from ..models.utils_model import LLMRequest
-from src.common.logger import get_module_logger, LogConfig, EMOJI_STYLE_CONFIG
-
-emoji_log_config = LogConfig(
- console_format=EMOJI_STYLE_CONFIG["console_format"],
- file_format=EMOJI_STYLE_CONFIG["file_format"],
-)
-
-logger = get_module_logger("emoji", config=emoji_log_config)
-
-
-image_manager = ImageManager()
-
-
-class EmojiManager:
- _instance = None
- EMOJI_DIR = os.path.join("data", "emoji") # 表情包存储目录
-
- def __new__(cls):
- if cls._instance is None:
- cls._instance = super().__new__(cls)
- cls._instance._initialized = False
- return cls._instance
-
- def __init__(self):
- self._scan_task = None
- self.vlm = LLMRequest(model=global_config.vlm, temperature=0.3, max_tokens=1000, request_type="emoji")
- self.llm_emotion_judge = LLMRequest(
- model=global_config.llm_emotion_judge, max_tokens=600, temperature=0.8, request_type="emoji"
- ) # 更高的温度,更少的token(后续可以根据情绪来调整温度)
-
- self.emoji_num = 0
- self.emoji_num_max = global_config.max_emoji_num
- self.emoji_num_max_reach_deletion = global_config.max_reach_deletion
-
- logger.info("启动表情包管理器")
-
- def _ensure_emoji_dir(self):
- """确保表情存储目录存在"""
- os.makedirs(self.EMOJI_DIR, exist_ok=True)
-
- def _update_emoji_count(self):
- """更新表情包数量统计
-
- 检查数据库中的表情包数量并更新到 self.emoji_num
- """
- try:
- self._ensure_db()
- self.emoji_num = db.emoji.count_documents({})
- logger.info(f"[统计] 当前表情包数量: {self.emoji_num}")
- except Exception as e:
- logger.error(f"[错误] 更新表情包数量失败: {str(e)}")
-
- def initialize(self):
- """初始化数据库连接和表情目录"""
- if not self._initialized:
- try:
- self._ensure_emoji_collection()
- self._ensure_emoji_dir()
- self._initialized = True
- # 更新表情包数量
- self._update_emoji_count()
- # 启动时执行一次完整性检查
- self.check_emoji_file_integrity()
- except Exception:
- logger.exception("初始化表情管理器失败")
-
- def _ensure_db(self):
- """确保数据库已初始化"""
- if not self._initialized:
- self.initialize()
- if not self._initialized:
- raise RuntimeError("EmojiManager not initialized")
-
- @staticmethod
- def _ensure_emoji_collection():
- """确保emoji集合存在并创建索引
-
- 这个函数用于确保MongoDB数据库中存在emoji集合,并创建必要的索引。
-
- 索引的作用是加快数据库查询速度:
- - embedding字段的2dsphere索引: 用于加速向量相似度搜索,帮助快速找到相似的表情包
- - tags字段的普通索引: 加快按标签搜索表情包的速度
- - filename字段的唯一索引: 确保文件名不重复,同时加快按文件名查找的速度
-
- 没有索引的话,数据库每次查询都需要扫描全部数据,建立索引后可以大大提高查询效率。
- """
- if "emoji" not in db.list_collection_names():
- db.create_collection("emoji")
- db.emoji.create_index([("embedding", "2dsphere")])
- db.emoji.create_index([("filename", 1)], unique=True)
-
- def record_usage(self, emoji_id: str):
- """记录表情使用次数"""
- try:
- self._ensure_db()
- db.emoji.update_one({"_id": emoji_id}, {"$inc": {"usage_count": 1}})
- except Exception as e:
- logger.error(f"记录表情使用失败: {str(e)}")
-
- async def get_emoji_for_text(self, text: str) -> Optional[Tuple[str, str]]:
- """根据文本内容获取相关表情包
- Args:
- text: 输入文本
- Returns:
- Optional[str]: 表情包文件路径,如果没有找到则返回None
-
-
- 可不可以通过 配置文件中的指令 来自定义使用表情包的逻辑?
- 我觉得可行
-
- """
- try:
- self._ensure_db()
-
- # 获取文本的embedding
- text_for_search = await self._get_kimoji_for_text(text)
- if not text_for_search:
- logger.error("无法获取文本的情绪")
- return None
- text_embedding = await get_embedding(text_for_search, request_type="emoji")
- if not text_embedding:
- logger.error("无法获取文本的embedding")
- return None
-
- try:
- # 获取所有表情包
- all_emojis = [
- e
- for e in db.emoji.find({}, {"_id": 1, "path": 1, "embedding": 1, "description": 1, "blacklist": 1})
- if "blacklist" not in e
- ]
-
- if not all_emojis:
- logger.warning("数据库中没有任何表情包")
- return None
-
- # 计算余弦相似度并排序
- def cosine_similarity(v1, v2):
- if not v1 or not v2:
- return 0
- dot_product = sum(a * b for a, b in zip(v1, v2))
- norm_v1 = sum(a * a for a in v1) ** 0.5
- norm_v2 = sum(b * b for b in v2) ** 0.5
- if norm_v1 == 0 or norm_v2 == 0:
- return 0
- return dot_product / (norm_v1 * norm_v2)
-
- # 计算所有表情包与输入文本的相似度
- emoji_similarities = [
- (emoji, cosine_similarity(text_embedding, emoji.get("embedding", []))) for emoji in all_emojis
- ]
-
- # 按相似度降序排序
- emoji_similarities.sort(key=lambda x: x[1], reverse=True)
-
- # 获取前3个最相似的表情包
- top_10_emojis = emoji_similarities[: 10 if len(emoji_similarities) > 10 else len(emoji_similarities)]
-
- if not top_10_emojis:
- logger.warning("未找到匹配的表情包")
- return None
-
- # 从前3个中随机选择一个
- selected_emoji, similarity = random.choice(top_10_emojis)
-
- if selected_emoji and "path" in selected_emoji:
- # 更新使用次数
- db.emoji.update_one({"_id": selected_emoji["_id"]}, {"$inc": {"usage_count": 1}})
-
- logger.info(
- f"[匹配] 找到表情包: {selected_emoji.get('description', '无描述')} (相似度: {similarity:.4f})"
- )
- # 稍微改一下文本描述,不然容易产生幻觉,描述已经包含 表情包 了
- return selected_emoji["path"], "[ %s ]" % selected_emoji.get("description", "无描述")
-
- except Exception as search_error:
- logger.error(f"[错误] 搜索表情包失败: {str(search_error)}")
- return None
-
- return None
-
- except Exception as e:
- logger.error(f"[错误] 获取表情包失败: {str(e)}")
- return None
-
- @staticmethod
- async def _get_emoji_description(image_base64: str) -> str:
- """获取表情包的标签,使用image_manager的描述生成功能"""
-
- try:
- # 使用image_manager获取描述,去掉前后的方括号和"表情包:"前缀
- description = await image_manager.get_emoji_description(image_base64)
- # 去掉[表情包:xxx]的格式,只保留描述内容
- description = description.strip("[]").replace("表情包:", "")
- return description
-
- except Exception as e:
- logger.error(f"[错误] 获取表情包描述失败: {str(e)}")
- return None
-
- async def _check_emoji(self, image_base64: str, image_format: str) -> str:
- try:
- prompt = (
- f'这是一个表情包,请回答这个表情包是否满足"{global_config.EMOJI_CHECK_PROMPT}"的要求,是则回答是,'
- f"否则回答否,不要出现任何其他内容"
- )
-
- content, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format)
- logger.debug(f"[检查] 表情包检查结果: {content}")
- return content
-
- except Exception as e:
- logger.error(f"[错误] 表情包检查失败: {str(e)}")
- return None
-
- async def _get_kimoji_for_text(self, text: str):
- try:
- prompt = (
- f"这是{global_config.BOT_NICKNAME}将要发送的消息内容:\n{text}\n若要为其配上表情包,"
- f"请你输出这个表情包应该表达怎样的情感,应该给人什么样的感觉,不要太简洁也不要太长,"
- f'注意不要输出任何对消息内容的分析内容,只输出"一种什么样的感觉"中间的形容词部分。'
- )
-
- content, _ = await self.llm_emotion_judge.generate_response_async(prompt, temperature=1.5)
- logger.info(f"[情感] 表情包情感描述: {content}")
- return content
-
- except Exception as e:
- logger.error(f"[错误] 获取表情包情感失败: {str(e)}")
- return None
-
- async def scan_new_emojis(self):
- """扫描新的表情包"""
- try:
- emoji_dir = self.EMOJI_DIR
- os.makedirs(emoji_dir, exist_ok=True)
-
- # 获取所有支持的图片文件
- files_to_process = [
- f for f in os.listdir(emoji_dir) if f.lower().endswith((".jpg", ".jpeg", ".png", ".gif"))
- ]
-
- # 检查当前表情包数量
- self._update_emoji_count()
- if self.emoji_num >= self.emoji_num_max:
- logger.warning(f"[警告] 表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max}),跳过注册")
- return
-
- # 计算还可以注册的数量
- remaining_slots = self.emoji_num_max - self.emoji_num
- logger.info(f"[注册] 还可以注册 {remaining_slots} 个表情包")
-
- for filename in files_to_process:
- # 如果已经达到上限,停止注册
- if self.emoji_num >= self.emoji_num_max:
- logger.warning(f"[警告] 表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max}),停止注册")
- break
-
- image_path = os.path.join(emoji_dir, filename)
-
- # 获取图片的base64编码和哈希值
- image_base64 = image_path_to_base64(image_path)
- if image_base64 is None:
- os.remove(image_path)
- continue
-
- image_bytes = base64.b64decode(image_base64)
- image_hash = hashlib.md5(image_bytes).hexdigest()
- image_format = Image.open(io.BytesIO(image_bytes)).format.lower()
- # 检查是否已经注册过
- existing_emoji_by_path = db["emoji"].find_one({"filename": filename})
- existing_emoji_by_hash = db["emoji"].find_one({"hash": image_hash})
- if existing_emoji_by_path and existing_emoji_by_hash:
- if existing_emoji_by_path["_id"] != existing_emoji_by_hash["_id"]:
- logger.error(f"[错误] 表情包已存在但记录不一致: {filename}")
- db.emoji.delete_one({"_id": existing_emoji_by_path["_id"]})
- db.emoji.delete_one({"_id": existing_emoji_by_hash["_id"]})
- existing_emoji = None
- else:
- existing_emoji = existing_emoji_by_hash
- elif existing_emoji_by_hash:
- logger.error(f"[错误] 表情包hash已存在但path不存在: {filename}")
- db.emoji.delete_one({"_id": existing_emoji_by_hash["_id"]})
- existing_emoji = None
- elif existing_emoji_by_path:
- logger.error(f"[错误] 表情包path已存在但hash不存在: {filename}")
- db.emoji.delete_one({"_id": existing_emoji_by_path["_id"]})
- existing_emoji = None
- else:
- existing_emoji = None
-
- description = None
-
- if existing_emoji:
- # 即使表情包已存在,也检查是否需要同步到images集合
- description = existing_emoji.get("description")
- # 检查是否在images集合中存在
- existing_image = db.images.find_one({"hash": image_hash})
- if not existing_image:
- # 同步到images集合
- image_doc = {
- "hash": image_hash,
- "path": image_path,
- "type": "emoji",
- "description": description,
- "timestamp": int(time.time()),
- }
- db.images.update_one({"hash": image_hash}, {"$set": image_doc}, upsert=True)
- # 保存描述到image_descriptions集合
- image_manager._save_description_to_db(image_hash, description, "emoji")
- logger.success(f"[同步] 已同步表情包到images集合: {filename}")
- continue
-
- # 检查是否在images集合中已有描述
- existing_description = image_manager._get_description_from_db(image_hash, "emoji")
-
- if existing_description:
- description = existing_description
- else:
- # 获取表情包的描述
- description = await self._get_emoji_description(image_base64)
-
- if global_config.EMOJI_CHECK:
- check = await self._check_emoji(image_base64, image_format)
- if "是" not in check:
- os.remove(image_path)
- logger.info(f"[过滤] 表情包描述: {description}")
- logger.info(f"[过滤] 表情包不满足规则,已移除: {check}")
- continue
- logger.info(f"[检查] 表情包检查通过: {check}")
-
- if description is not None:
- embedding = await get_embedding(description, request_type="emoji")
- if not embedding:
- logger.error("获取消息嵌入向量失败")
- raise ValueError("获取消息嵌入向量失败")
- # 准备数据库记录
- emoji_record = {
- "filename": filename,
- "path": image_path,
- "embedding": embedding,
- "description": description,
- "hash": image_hash,
- "timestamp": int(time.time()),
- }
-
- # 保存到emoji数据库
- db["emoji"].insert_one(emoji_record)
- logger.success(f"[注册] 新表情包: {filename}")
- logger.info(f"[描述] {description}")
-
- # 更新当前表情包数量
- self.emoji_num += 1
- logger.info(f"[统计] 当前表情包数量: {self.emoji_num}/{self.emoji_num_max}")
-
- # 保存到images数据库
- image_doc = {
- "hash": image_hash,
- "path": image_path,
- "type": "emoji",
- "description": description,
- "timestamp": int(time.time()),
- }
- db.images.update_one({"hash": image_hash}, {"$set": image_doc}, upsert=True)
- # 保存描述到image_descriptions集合
- image_manager._save_description_to_db(image_hash, description, "emoji")
- logger.success(f"[同步] 已保存到images集合: {filename}")
- else:
- logger.warning(f"[跳过] 表情包: {filename}")
-
- except Exception:
- logger.exception("[错误] 扫描表情包失败")
-
- def check_emoji_file_integrity(self):
- """检查表情包文件完整性
- 如果文件已被删除,则从数据库中移除对应记录
- """
- try:
- self._ensure_db()
- # 获取所有表情包记录
- all_emojis = list(db.emoji.find())
- removed_count = 0
- total_count = len(all_emojis)
-
- for emoji in all_emojis:
- try:
- if "path" not in emoji:
- logger.warning(f"[检查] 发现无效记录(缺少path字段),ID: {emoji.get('_id', 'unknown')}")
- db.emoji.delete_one({"_id": emoji["_id"]})
- removed_count += 1
- continue
-
- if "embedding" not in emoji:
- logger.warning(f"[检查] 发现过时记录(缺少embedding字段),ID: {emoji.get('_id', 'unknown')}")
- db.emoji.delete_one({"_id": emoji["_id"]})
- removed_count += 1
- continue
-
- # 检查文件是否存在
- if not os.path.exists(emoji["path"]):
- logger.warning(f"[检查] 表情包文件已被删除: {emoji['path']}")
- # 从数据库中删除记录
- result = db.emoji.delete_one({"_id": emoji["_id"]})
- if result.deleted_count > 0:
- logger.debug(f"[清理] 成功删除数据库记录: {emoji['_id']}")
- removed_count += 1
- else:
- logger.error(f"[错误] 删除数据库记录失败: {emoji['_id']}")
- continue
-
- if "hash" not in emoji:
- logger.warning(f"[检查] 发现缺失记录(缺少hash字段),ID: {emoji.get('_id', 'unknown')}")
- hash = hashlib.md5(open(emoji["path"], "rb").read()).hexdigest()
- db.emoji.update_one({"_id": emoji["_id"]}, {"$set": {"hash": hash}})
- else:
- file_hash = hashlib.md5(open(emoji["path"], "rb").read()).hexdigest()
- if emoji["hash"] != file_hash:
- logger.warning(f"[检查] 表情包文件hash不匹配,ID: {emoji.get('_id', 'unknown')}")
- db.emoji.delete_one({"_id": emoji["_id"]})
- removed_count += 1
-
- # 修复拼写错误
- if "discription" in emoji:
- desc = emoji["discription"]
- db.emoji.update_one(
- {"_id": emoji["_id"]}, {"$unset": {"discription": ""}, "$set": {"description": desc}}
- )
-
- except Exception as item_error:
- logger.error(f"[错误] 处理表情包记录时出错: {str(item_error)}")
- continue
-
- # 验证清理结果
- remaining_count = db.emoji.count_documents({})
- if removed_count > 0:
- logger.success(f"[清理] 已清理 {removed_count} 个失效的表情包记录")
- logger.info(f"[统计] 清理前: {total_count} | 清理后: {remaining_count}")
- else:
- logger.info(f"[检查] 已检查 {total_count} 个表情包记录")
-
- except Exception as e:
- logger.error(f"[错误] 检查表情包完整性失败: {str(e)}")
- logger.error(traceback.format_exc())
-
- def check_emoji_file_full(self):
- """检查表情包文件是否完整,如果数量超出限制且允许删除,则删除多余的表情包
-
- 删除规则:
- 1. 优先删除创建时间更早的表情包
- 2. 优先删除使用次数少的表情包,但使用次数多的也有小概率被删除
- """
- try:
- self._ensure_db()
- # 更新表情包数量
- self._update_emoji_count()
-
- # 检查是否超出限制
- if self.emoji_num <= self.emoji_num_max:
- return
-
- # 如果超出限制但不允许删除,则只记录警告
- if not global_config.max_reach_deletion:
- logger.warning(f"[警告] 表情包数量({self.emoji_num})超出限制({self.emoji_num_max}),但未开启自动删除")
- return
-
- # 计算需要删除的数量
- delete_count = self.emoji_num - self.emoji_num_max
- logger.info(f"[清理] 需要删除 {delete_count} 个表情包")
-
- # 获取所有表情包,按时间戳升序(旧的在前)排序
- all_emojis = list(db.emoji.find().sort([("timestamp", 1)]))
-
- # 计算权重:使用次数越多,被删除的概率越小
- weights = []
- max_usage = max((emoji.get("usage_count", 0) for emoji in all_emojis), default=1)
- for emoji in all_emojis:
- usage_count = emoji.get("usage_count", 0)
- # 使用指数衰减函数计算权重,使用次数越多权重越小
- weight = 1.0 / (1.0 + usage_count / max(1, max_usage))
- weights.append(weight)
-
- # 根据权重随机选择要删除的表情包
- to_delete = []
- remaining_indices = list(range(len(all_emojis)))
-
- while len(to_delete) < delete_count and remaining_indices:
- # 计算当前剩余表情包的权重
- current_weights = [weights[i] for i in remaining_indices]
- # 归一化权重
- total_weight = sum(current_weights)
- if total_weight == 0:
- break
- normalized_weights = [w / total_weight for w in current_weights]
-
- # 随机选择一个表情包
- selected_idx = random.choices(remaining_indices, weights=normalized_weights, k=1)[0]
- to_delete.append(all_emojis[selected_idx])
- remaining_indices.remove(selected_idx)
-
- # 删除选中的表情包
- deleted_count = 0
- for emoji in to_delete:
- try:
- # 删除文件
- if "path" in emoji and os.path.exists(emoji["path"]):
- os.remove(emoji["path"])
- logger.info(f"[删除] 文件: {emoji['path']} (使用次数: {emoji.get('usage_count', 0)})")
-
- # 删除数据库记录
- db.emoji.delete_one({"_id": emoji["_id"]})
- deleted_count += 1
-
- # 同时从images集合中删除
- if "hash" in emoji:
- db.images.delete_one({"hash": emoji["hash"]})
-
- except Exception as e:
- logger.error(f"[错误] 删除表情包失败: {str(e)}")
- continue
-
- # 更新表情包数量
- self._update_emoji_count()
- logger.success(f"[清理] 已删除 {deleted_count} 个表情包,当前数量: {self.emoji_num}")
-
- except Exception as e:
- logger.error(f"[错误] 检查表情包数量失败: {str(e)}")
-
- async def start_periodic_check_register(self):
- """定期检查表情包完整性和数量"""
- while True:
- logger.info("[扫描] 开始检查表情包完整性...")
- self.check_emoji_file_integrity()
- logger.info("[扫描] 开始删除所有图片缓存...")
- await self.delete_all_images()
- logger.info("[扫描] 开始扫描新表情包...")
- if self.emoji_num < self.emoji_num_max:
- await self.scan_new_emojis()
- if self.emoji_num > self.emoji_num_max:
- logger.warning(f"[警告] 表情包数量超过最大限制: {self.emoji_num} > {self.emoji_num_max},跳过注册")
- if not global_config.max_reach_deletion:
- logger.warning("表情包数量超过最大限制,终止注册")
- break
- else:
- logger.warning("表情包数量超过最大限制,开始删除表情包")
- self.check_emoji_file_full()
- await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
-
- @staticmethod
- async def delete_all_images():
- """删除 data/image 目录下的所有文件"""
- try:
- image_dir = os.path.join("data", "image")
- if not os.path.exists(image_dir):
- logger.warning(f"[警告] 目录不存在: {image_dir}")
- return
-
- deleted_count = 0
- failed_count = 0
-
- # 遍历目录下的所有文件
- for filename in os.listdir(image_dir):
- file_path = os.path.join(image_dir, filename)
- try:
- if os.path.isfile(file_path):
- os.remove(file_path)
- deleted_count += 1
- logger.debug(f"[删除] 文件: {file_path}")
- except Exception as e:
- failed_count += 1
- logger.error(f"[错误] 删除文件失败 {file_path}: {str(e)}")
-
- logger.success(f"[清理] 已删除 {deleted_count} 个文件,失败 {failed_count} 个")
-
- except Exception as e:
- logger.error(f"[错误] 删除图片目录失败: {str(e)}")
-
-
-# 创建全局单例
-emoji_manager = EmojiManager()
diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py
index 2ba645f95..c7f7ac83e 100644
--- a/src/plugins/chat/message.py
+++ b/src/plugins/chat/message.py
@@ -7,7 +7,7 @@ import urllib3
from src.common.logger import get_module_logger
from .chat_stream import ChatStream
from .utils_image import image_manager
-from ..message.message_base import Seg, UserInfo, BaseMessageInfo, MessageBase
+from maim_message import Seg, UserInfo, BaseMessageInfo, MessageBase
logger = get_module_logger("chat_message")
@@ -127,12 +127,12 @@ class MessageRecv(Message):
# 如果是base64图片数据
if isinstance(seg.data, str):
return await image_manager.get_image_description(seg.data)
- return "[图片]"
+ return "[发了一张图片,网卡了加载不出来]"
elif seg.type == "emoji":
self.is_emoji = True
if isinstance(seg.data, str):
return await image_manager.get_emoji_description(seg.data)
- return "[表情]"
+ return "[发了一个表情包,网卡了加载不出来]"
else:
return f"[{seg.type}:{str(seg.data)}]"
except Exception as e:
@@ -141,14 +141,8 @@ class MessageRecv(Message):
def _generate_detailed_text(self) -> str:
"""生成详细文本,包含时间和用户信息"""
- # time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(self.message_info.time))
timestamp = self.message_info.time
user_info = self.message_info.user_info
- # name = (
- # f"{user_info.user_nickname}(ta的昵称:{user_info.user_cardname},ta的id:{user_info.user_id})"
- # if user_info.user_cardname != None
- # else f"{user_info.user_nickname}(ta的id:{user_info.user_id})"
- # )
name = f"<{self.message_info.platform}:{user_info.user_id}:{user_info.user_nickname}:{user_info.user_cardname}>"
return f"[{timestamp}] {name}: {self.processed_plain_text}\n"
@@ -222,11 +216,11 @@ class MessageProcessBase(Message):
# 如果是base64图片数据
if isinstance(seg.data, str):
return await image_manager.get_image_description(seg.data)
- return "[图片]"
+ return "[图片,网卡了加载不出来]"
elif seg.type == "emoji":
if isinstance(seg.data, str):
return await image_manager.get_emoji_description(seg.data)
- return "[表情]"
+ return "[表情,网卡了加载不出来]"
elif seg.type == "at":
return f"[@{seg.data}]"
elif seg.type == "reply":
diff --git a/src/plugins/chat/message_buffer.py b/src/plugins/chat/message_buffer.py
index d0ab56042..d76d23289 100644
--- a/src/plugins/chat/message_buffer.py
+++ b/src/plugins/chat/message_buffer.py
@@ -3,7 +3,7 @@ from src.common.logger import get_module_logger
import asyncio
from dataclasses import dataclass, field
from .message import MessageRecv
-from ..message.message_base import BaseMessageInfo, GroupInfo, Seg
+from maim_message import BaseMessageInfo, GroupInfo
import hashlib
from typing import Dict
from collections import OrderedDict
@@ -128,58 +128,67 @@ class MessageBuffer:
if result:
async with self.lock: # 再次加锁
# 清理所有早于当前消息的已处理消息, 收集所有早于当前消息的F消息的processed_plain_text
- keep_msgs = OrderedDict()
- combined_text = []
- found = False
- type = "seglist"
- is_update = True
- for msg_id, msg in self.buffer_pool[person_id_].items():
+ keep_msgs = OrderedDict() # 用于存放 T 消息之后的消息
+ collected_texts = [] # 用于收集 T 消息及之前 F 消息的文本
+ process_target_found = False
+
+ # 遍历当前用户的所有缓冲消息
+ for msg_id, cache_msg in self.buffer_pool[person_id_].items():
+ # 如果找到了目标处理消息 (T 状态)
if msg_id == message.message_info.message_id:
- found = True
- if msg.message.message_segment.type != "seglist":
- type = msg.message.message_segment.type
- else:
- if (
- isinstance(msg.message.message_segment.data, list)
- and all(isinstance(x, Seg) for x in msg.message.message_segment.data)
- and len(msg.message.message_segment.data) == 1
- ):
- type = msg.message.message_segment.data[0].type
- combined_text.append(msg.message.processed_plain_text)
- continue
- if found:
- keep_msgs[msg_id] = msg
- elif msg.result == "F":
- # 收集F消息的文本内容
- f_type = "seglist"
- if msg.message.message_segment.type != "seglist":
- f_type = msg.message.message_segment.type
- else:
- if (
- isinstance(msg.message.message_segment.data, list)
- and all(isinstance(x, Seg) for x in msg.message.message_segment.data)
- and len(msg.message.message_segment.data) == 1
- ):
- f_type = msg.message.message_segment.data[0].type
- if hasattr(msg.message, "processed_plain_text") and msg.message.processed_plain_text:
- if f_type == "text":
- combined_text.append(msg.message.processed_plain_text)
- elif f_type != "text":
- is_update = False
- elif msg.result == "U":
- logger.debug(f"异常未处理信息id: {msg.message.message_info.message_id}")
+ process_target_found = True
+ # 收集这条 T 消息的文本 (如果有)
+ if (
+ hasattr(cache_msg.message, "processed_plain_text")
+ and cache_msg.message.processed_plain_text
+ ):
+ collected_texts.append(cache_msg.message.processed_plain_text)
+ # 不立即放入 keep_msgs,因为它之前的 F 消息也处理完了
- # 更新当前消息的processed_plain_text
- if combined_text and combined_text[0] != message.processed_plain_text and is_update:
- if type == "text":
- message.processed_plain_text = ",".join(combined_text)
- logger.debug(f"整合了{len(combined_text) - 1}条F消息的内容到当前消息")
- elif type == "emoji":
- combined_text.pop()
- message.processed_plain_text = ",".join(combined_text)
- message.is_emoji = False
- logger.debug(f"整合了{len(combined_text) - 1}条F消息的内容,覆盖当前emoji消息")
+ # 如果已经找到了目标 T 消息,之后的消息需要保留
+ elif process_target_found:
+ keep_msgs[msg_id] = cache_msg
+ # 如果还没找到目标 T 消息,说明是之前的消息 (F 或 U)
+ else:
+ if cache_msg.result == "F":
+ # 收集这条 F 消息的文本 (如果有)
+ if (
+ hasattr(cache_msg.message, "processed_plain_text")
+ and cache_msg.message.processed_plain_text
+ ):
+ collected_texts.append(cache_msg.message.processed_plain_text)
+ elif cache_msg.result == "U":
+ # 理论上不应该在 T 消息之前还有 U 消息,记录日志
+ logger.warning(
+ f"异常状态:在目标 T 消息 {message.message_info.message_id} 之前发现未处理的 U 消息 {cache_msg.message.message_info.message_id}"
+ )
+ # 也可以选择收集其文本
+ if (
+ hasattr(cache_msg.message, "processed_plain_text")
+ and cache_msg.message.processed_plain_text
+ ):
+ collected_texts.append(cache_msg.message.processed_plain_text)
+
+ # 更新当前消息 (message) 的 processed_plain_text
+ # 只有在收集到的文本多于一条,或者只有一条但与原始文本不同时才合并
+ if collected_texts:
+ # 使用 OrderedDict 去重,同时保留原始顺序
+ unique_texts = list(OrderedDict.fromkeys(collected_texts))
+ merged_text = ",".join(unique_texts)
+
+ # 只有在合并后的文本与原始文本不同时才更新
+ # 并且确保不是空合并
+ if merged_text and merged_text != message.processed_plain_text:
+ message.processed_plain_text = merged_text
+ # 如果合并了文本,原消息不再视为纯 emoji
+ if hasattr(message, "is_emoji"):
+ message.is_emoji = False
+ logger.debug(
+ f"合并了 {len(unique_texts)} 条消息的文本内容到当前消息 {message.message_info.message_id}"
+ )
+
+ # 更新缓冲池,只保留 T 消息之后的消息
self.buffer_pool[person_id_] = keep_msgs
return result
except asyncio.TimeoutError:
diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py
index a737d99cf..d51492f70 100644
--- a/src/plugins/chat/message_sender.py
+++ b/src/plugins/chat/message_sender.py
@@ -62,20 +62,10 @@ class MessageSender:
# logger.trace(f"{message.processed_plain_text},{typing_time},等待输入时间结束") # 减少日志
# --- 结束打字延迟 ---
- message_json = message.to_dict()
message_preview = truncate_message(message.processed_plain_text)
try:
- end_point = global_config.api_urls.get(message.message_info.platform, None)
- if end_point:
- try:
- await global_api.send_message_rest(end_point, message_json)
- except Exception as e:
- logger.error(f"REST发送失败: {str(e)}")
- logger.info(f"[{message.chat_stream.stream_id}] 尝试使用WS发送")
- await self.send_via_ws(message)
- else:
- await self.send_via_ws(message)
+ await self.send_via_ws(message)
logger.success(f"发送消息 '{message_preview}' 成功") # 调整日志格式
except Exception as e:
logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}")
diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py
index aed0025b8..4eb3d93dd 100644
--- a/src/plugins/chat/utils.py
+++ b/src/plugins/chat/utils.py
@@ -12,7 +12,7 @@ from ..models.utils_model import LLMRequest
from ..utils.typo_generator import ChineseTypoGenerator
from ...config.config import global_config
from .message import MessageRecv, Message
-from ..message.message_base import UserInfo
+from maim_message import UserInfo
from .chat_stream import ChatStream
from ..moods.moods import MoodManager
from ...common.database import db
@@ -234,6 +234,13 @@ def split_into_sentences_w_remove_punctuation(text: str) -> List[str]:
Returns:
List[str]: 分割和合并后的句子列表
"""
+ # 预处理:处理多余的换行符
+ # 1. 将连续的换行符替换为单个换行符
+ text = re.sub(r"\n\s*\n+", "\n", text)
+ # 2. 处理换行符和其他分隔符的组合
+ text = re.sub(r"\n\s*([,,。;\s])", r"\1", text)
+ text = re.sub(r"([,,。;\s])\s*\n", r"\1", text)
+
# 处理两个汉字中间的换行符
text = re.sub(r"([\u4e00-\u9fff])\n([\u4e00-\u9fff])", r"\1。\2", text)
@@ -365,12 +372,16 @@ def random_remove_punctuation(text: str) -> str:
def process_llm_response(text: str) -> List[str]:
# 先保护颜文字
- protected_text, kaomoji_mapping = protect_kaomoji(text)
- logger.trace(f"保护颜文字后的文本: {protected_text}")
+ if global_config.enable_kaomoji_protection:
+ protected_text, kaomoji_mapping = protect_kaomoji(text)
+ logger.trace(f"保护颜文字后的文本: {protected_text}")
+ else:
+ protected_text = text
+ kaomoji_mapping = {}
# 提取被 () 或 [] 包裹且包含中文的内容
pattern = re.compile(r"[\(\[\(](?=.*[\u4e00-\u9fff]).*?[\)\]\)]")
# _extracted_contents = pattern.findall(text)
- extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找
+ _extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找
# 去除 () 和 [] 及其包裹的内容
cleaned_text = pattern.sub("", protected_text)
@@ -413,13 +424,14 @@ def process_llm_response(text: str) -> List[str]:
if len(sentences) > max_sentence_num:
logger.warning(f"分割后消息数量过多 ({len(sentences)} 条),返回默认回复")
return [f"{global_config.BOT_NICKNAME}不知道哦"]
- if extracted_contents:
- for content in extracted_contents:
- sentences.append(content)
- # 在所有句子处理完毕后,对包含占位符的列表进行恢复
- sentences = recover_kaomoji(sentences, kaomoji_mapping)
- print(sentences)
+ # if extracted_contents:
+ # for content in extracted_contents:
+ # sentences.append(content)
+
+ # 在所有句子处理完毕后,对包含占位符的列表进行恢复
+ if global_config.enable_kaomoji_protection:
+ sentences = recover_kaomoji(sentences, kaomoji_mapping)
return sentences
diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py
index 9c7a03b06..f6b9231ad 100644
--- a/src/plugins/chat/utils_image.py
+++ b/src/plugins/chat/utils_image.py
@@ -121,7 +121,7 @@ class ImageManager:
prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,使用1-2个词描述一下表情包表达的情感和内容,简短一些"
description, _ = await self._llm.generate_response_for_image(prompt, image_base64, "jpg")
else:
- prompt = "这是一个表情包,请用使用1-2个词描述一下表情包所表达的情感和内容,简短一些"
+ prompt = "这是一个表情包,请用使用几个词描述一下表情包所表达的情感和内容,简短一些"
description, _ = await self._llm.generate_response_for_image(prompt, image_base64, image_format)
cached_description = self._get_description_from_db(image_hash, "emoji")
@@ -130,7 +130,7 @@ class ImageManager:
return f"[表达了:{cached_description}]"
# 根据配置决定是否保存图片
- if global_config.EMOJI_SAVE:
+ if global_config.save_emoji:
# 生成文件名和路径
timestamp = int(time.time())
filename = f"{timestamp}_{image_hash[:8]}.{image_format}"
@@ -152,7 +152,7 @@ class ImageManager:
"timestamp": timestamp,
}
db.images.update_one({"hash": image_hash}, {"$set": image_doc}, upsert=True)
- logger.success(f"保存表情包: {file_path}")
+ logger.trace(f"保存表情包: {file_path}")
except Exception as e:
logger.error(f"保存表情包文件失败: {str(e)}")
@@ -196,7 +196,7 @@ class ImageManager:
return "[图片]"
# 根据配置决定是否保存图片
- if global_config.EMOJI_SAVE:
+ if global_config.save_pic:
# 生成文件名和路径
timestamp = int(time.time())
filename = f"{timestamp}_{image_hash[:8]}.{image_format}"
@@ -309,11 +309,15 @@ def image_path_to_base64(image_path: str) -> str:
image_path: 图片文件路径
Returns:
str: base64编码的图片数据
+ Raises:
+ FileNotFoundError: 当图片文件不存在时
+ IOError: 当读取图片文件失败时
"""
- try:
- with open(image_path, "rb") as f:
- image_data = f.read()
- return base64.b64encode(image_data).decode("utf-8")
- except Exception as e:
- logger.error(f"读取图片失败: {image_path}, 错误: {str(e)}")
- return None
+ if not os.path.exists(image_path):
+ raise FileNotFoundError(f"图片文件不存在: {image_path}")
+
+ with open(image_path, "rb") as f:
+ image_data = f.read()
+ if not image_data:
+ raise IOError(f"读取图片文件失败: {image_path}")
+ return base64.b64encode(image_data).decode("utf-8")
diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py
new file mode 100644
index 000000000..cf3ebadb8
--- /dev/null
+++ b/src/plugins/emoji_system/emoji_manager.py
@@ -0,0 +1,827 @@
+import asyncio
+import base64
+import hashlib
+import os
+import random
+import time
+import traceback
+from typing import Optional, Tuple
+from PIL import Image
+import io
+import re
+
+from ...common.database import db
+from ...config.config import global_config
+from ..chat.utils_image import image_path_to_base64, image_manager
+from ..models.utils_model import LLMRequest
+from src.common.logger import get_module_logger, LogConfig, EMOJI_STYLE_CONFIG
+
+
+emoji_log_config = LogConfig(
+ console_format=EMOJI_STYLE_CONFIG["console_format"],
+ file_format=EMOJI_STYLE_CONFIG["file_format"],
+)
+
+logger = get_module_logger("emoji", config=emoji_log_config)
+
+BASE_DIR = os.path.join("data")
+EMOJI_DIR = os.path.join(BASE_DIR, "emoji") # 表情包存储目录
+EMOJI_REGISTED_DIR = os.path.join(BASE_DIR, "emoji_registed") # 已注册的表情包注册目录
+
+
+"""
+还没经过测试,有些地方数据库和内存数据同步可能不完全
+
+"""
+
+
+class MaiEmoji:
+ """定义一个表情包"""
+
+ def __init__(self, filename: str, path: str):
+ self.path = path # 存储目录路径
+ self.filename = filename
+ self.embedding = []
+ self.hash = "" # 初始为空,在创建实例时会计算
+ self.description = ""
+ self.emotion = []
+ self.usage_count = 0
+ self.last_used_time = time.time()
+ self.register_time = time.time()
+ self.is_deleted = False # 标记是否已被删除
+ self.format = ""
+
+ async def initialize_hash_format(self):
+ """从文件创建表情包实例
+
+ 参数:
+ file_path: 文件的完整路径
+
+ 返回:
+ MaiEmoji: 创建的表情包实例,如果失败则返回None
+ """
+ try:
+ file_path = os.path.join(self.path, self.filename)
+ if not os.path.exists(file_path):
+ logger.error(f"[错误] 表情包文件不存在: {file_path}")
+ return None
+
+ image_base64 = image_path_to_base64(file_path)
+ if image_base64 is None:
+ logger.error(f"[错误] 无法读取图片: {file_path}")
+ return None
+
+ # 计算哈希值
+ image_bytes = base64.b64decode(image_base64)
+ self.hash = hashlib.md5(image_bytes).hexdigest()
+
+ # 获取图片格式
+ self.format = Image.open(io.BytesIO(image_bytes)).format.lower()
+
+ except Exception as e:
+ logger.error(f"[错误] 初始化表情包失败: {str(e)}")
+ logger.error(traceback.format_exc())
+ return None
+
+ async def register_to_db(self):
+ """
+ 注册表情包
+ 将表情包对应的文件,从当前路径移动到EMOJI_REGISTED_DIR目录下
+ 并修改对应的实例属性,然后将表情包信息保存到数据库中
+ """
+ try:
+ # 确保目标目录存在
+ os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True)
+
+ # 源路径是当前实例的完整路径
+ source_path = os.path.join(self.path, self.filename)
+ # 目标路径
+ destination_path = os.path.join(EMOJI_REGISTED_DIR, self.filename)
+
+ # 检查源文件是否存在
+ if not os.path.exists(source_path):
+ logger.error(f"[错误] 源文件不存在: {source_path}")
+ return False
+
+ # --- 文件移动 ---
+ try:
+ # 如果目标文件已存在,先删除 (确保移动成功)
+ if os.path.exists(destination_path):
+ os.remove(destination_path)
+
+ os.rename(source_path, destination_path)
+ logger.info(f"[移动] 文件从 {source_path} 移动到 {destination_path}")
+ # 更新实例的路径属性为新目录
+ self.path = EMOJI_REGISTED_DIR
+ except Exception as move_error:
+ logger.error(f"[错误] 移动文件失败: {str(move_error)}")
+ return False # 文件移动失败,不继续
+
+ # --- 数据库操作 ---
+ try:
+ # 准备数据库记录 for emoji collection
+ emoji_record = {
+ "filename": self.filename,
+ "path": os.path.join(self.path, self.filename), # 使用更新后的路径
+ "embedding": self.embedding,
+ "description": self.description,
+ "emotion": self.emotion, # 添加情感标签字段
+ "hash": self.hash,
+ "format": self.format,
+ "timestamp": int(self.register_time), # 使用实例的注册时间
+ "usage_count": self.usage_count,
+ "last_used_time": self.last_used_time,
+ }
+
+ # 使用upsert确保记录存在或被更新
+ db["emoji"].update_one({"hash": self.hash}, {"$set": emoji_record}, upsert=True)
+ logger.success(f"[注册] 表情包信息保存到数据库: {self.description}")
+
+ return True
+
+ except Exception as db_error:
+ logger.error(f"[错误] 保存数据库失败: {str(db_error)}")
+ # 考虑是否需要将文件移回?为了简化,暂时只记录错误
+ return False
+
+ except Exception as e:
+ logger.error(f"[错误] 注册表情包失败: {str(e)}")
+ logger.error(traceback.format_exc())
+ return False
+
+ async def delete(self):
+ """删除表情包
+
+ 删除表情包的文件和数据库记录
+
+ 返回:
+ bool: 是否成功删除
+ """
+ try:
+ # 1. 删除文件
+ if os.path.exists(os.path.join(self.path, self.filename)):
+ try:
+ os.remove(os.path.join(self.path, self.filename))
+ logger.info(f"[删除] 文件: {os.path.join(self.path, self.filename)}")
+ except Exception as e:
+ logger.error(f"[错误] 删除文件失败 {os.path.join(self.path, self.filename)}: {str(e)}")
+ # 继续执行,即使文件删除失败也尝试删除数据库记录
+
+ # 2. 删除数据库记录
+ result = db.emoji.delete_one({"hash": self.hash})
+ deleted_in_db = result.deleted_count > 0
+
+ if deleted_in_db:
+ logger.success(f"[删除] 成功删除表情包记录: {self.description}")
+
+ # 3. 标记对象已被删除
+ self.is_deleted = True
+ return True
+ else:
+ logger.error(f"[错误] 删除表情包记录失败: {self.hash}")
+ return False
+
+ except Exception as e:
+ logger.error(f"[错误] 删除表情包失败: {str(e)}")
+ return False
+
+
+class EmojiManager:
+ _instance = None
+
+ def __new__(cls):
+ if cls._instance is None:
+ cls._instance = super().__new__(cls)
+ cls._instance._initialized = False
+ return cls._instance
+
+ def __init__(self):
+ self._scan_task = None
+ self.vlm = LLMRequest(model=global_config.vlm, temperature=0.3, max_tokens=1000, request_type="emoji")
+ self.llm_emotion_judge = LLMRequest(
+ model=global_config.llm_emotion_judge, max_tokens=600, temperature=0.8, request_type="emoji"
+ ) # 更高的温度,更少的token(后续可以根据情绪来调整温度)
+
+ self.emoji_num = 0
+ self.emoji_num_max = global_config.max_emoji_num
+ self.emoji_num_max_reach_deletion = global_config.max_reach_deletion
+ self.emoji_objects: list[MaiEmoji] = [] # 存储MaiEmoji对象的列表,使用类型注解明确列表元素类型
+
+ logger.info("启动表情包管理器")
+
+ def _ensure_emoji_dir(self):
+ """确保表情存储目录存在"""
+ os.makedirs(EMOJI_DIR, exist_ok=True)
+
+ def initialize(self):
+ """初始化数据库连接和表情目录"""
+ if not self._initialized:
+ try:
+ self._ensure_emoji_collection()
+ self._ensure_emoji_dir()
+ self._initialized = True
+ # 更新表情包数量
+ # 启动时执行一次完整性检查
+ # await self.check_emoji_file_integrity()
+ except Exception:
+ logger.exception("初始化表情管理器失败")
+
+ def _ensure_db(self):
+ """确保数据库已初始化"""
+ if not self._initialized:
+ self.initialize()
+ if not self._initialized:
+ raise RuntimeError("EmojiManager not initialized")
+
+ @staticmethod
+ def _ensure_emoji_collection():
+ """确保emoji集合存在并创建索引
+
+ 这个函数用于确保MongoDB数据库中存在emoji集合,并创建必要的索引。
+
+ 索引的作用是加快数据库查询速度:
+ - embedding字段的2dsphere索引: 用于加速向量相似度搜索,帮助快速找到相似的表情包
+ - tags字段的普通索引: 加快按标签搜索表情包的速度
+ - filename字段的唯一索引: 确保文件名不重复,同时加快按文件名查找的速度
+
+ 没有索引的话,数据库每次查询都需要扫描全部数据,建立索引后可以大大提高查询效率。
+ """
+ if "emoji" not in db.list_collection_names():
+ db.create_collection("emoji")
+ db.emoji.create_index([("embedding", "2dsphere")])
+ db.emoji.create_index([("filename", 1)], unique=True)
+
+ def record_usage(self, hash: str):
+ """记录表情使用次数"""
+ try:
+ db.emoji.update_one({"hash": hash}, {"$inc": {"usage_count": 1}})
+ for emoji in self.emoji_objects:
+ if emoji.hash == hash:
+ emoji.usage_count += 1
+ break
+
+ except Exception as e:
+ logger.error(f"记录表情使用失败: {str(e)}")
+
+ async def get_emoji_for_text(self, text_emotion: str) -> Optional[Tuple[str, str]]:
+ """根据文本内容获取相关表情包
+ Args:
+ text_emotion: 输入的情感描述文本
+ Returns:
+ Optional[Tuple[str, str]]: (表情包文件路径, 表情包描述),如果没有找到则返回None
+ """
+ try:
+ self._ensure_db()
+ time_start = time.time()
+
+ # 获取所有表情包
+ all_emojis = self.emoji_objects
+
+ if not all_emojis:
+ logger.warning("数据库中没有任何表情包")
+ return None
+
+ # 计算每个表情包与输入文本的最大情感相似度
+ emoji_similarities = []
+ for emoji in all_emojis:
+ emotions = emoji.emotion
+ if not emotions:
+ continue
+
+ # 计算与每个emotion标签的相似度,取最大值
+ max_similarity = 0
+ for emotion in emotions:
+ # 使用编辑距离计算相似度
+ distance = self._levenshtein_distance(text_emotion, emotion)
+ max_len = max(len(text_emotion), len(emotion))
+ similarity = 1 - (distance / max_len if max_len > 0 else 0)
+ max_similarity = max(max_similarity, similarity)
+
+ emoji_similarities.append((emoji, max_similarity))
+
+ # 按相似度降序排序
+ emoji_similarities.sort(key=lambda x: x[1], reverse=True)
+
+ # 获取前5个最相似的表情包
+ top_5_emojis = emoji_similarities[:10] if len(emoji_similarities) > 10 else emoji_similarities
+
+ if not top_5_emojis:
+ logger.warning("未找到匹配的表情包")
+ return None
+
+ # 从前5个中随机选择一个
+ selected_emoji, similarity = random.choice(top_5_emojis)
+
+ # 更新使用次数
+ self.record_usage(selected_emoji.hash)
+
+ time_end = time.time()
+
+ logger.info(
+ f"找到[{text_emotion}]表情包,用时:{time_end - time_start:.2f}秒: {selected_emoji.description} (相似度: {similarity:.4f})"
+ )
+ return selected_emoji.path, f"[ {selected_emoji.description} ]"
+
+ except Exception as e:
+ logger.error(f"[错误] 获取表情包失败: {str(e)}")
+ return None
+
+ def _levenshtein_distance(self, s1: str, s2: str) -> int:
+ """计算两个字符串的编辑距离
+
+ Args:
+ s1: 第一个字符串
+ s2: 第二个字符串
+
+ Returns:
+ int: 编辑距离
+ """
+ if len(s1) < len(s2):
+ return self._levenshtein_distance(s2, s1)
+
+ if len(s2) == 0:
+ return len(s1)
+
+ previous_row = range(len(s2) + 1)
+ for i, c1 in enumerate(s1):
+ current_row = [i + 1]
+ for j, c2 in enumerate(s2):
+ insertions = previous_row[j + 1] + 1
+ deletions = current_row[j] + 1
+ substitutions = previous_row[j] + (c1 != c2)
+ current_row.append(min(insertions, deletions, substitutions))
+ previous_row = current_row
+
+ return previous_row[-1]
+
+ async def check_emoji_file_integrity(self):
+ """检查表情包文件完整性
+ 遍历self.emoji_objects中的所有对象,检查文件是否存在
+ 如果文件已被删除,则执行对象的删除方法并从列表中移除
+ """
+ try:
+ if not self.emoji_objects:
+ logger.warning("[检查] emoji_objects为空,跳过完整性检查")
+ return
+
+ total_count = len(self.emoji_objects)
+ removed_count = 0
+ # 使用列表复制进行遍历,因为我们会在遍历过程中修改列表
+ for emoji in self.emoji_objects[:]:
+ try:
+ # 检查文件是否存在
+ if not os.path.exists(emoji.path):
+ logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}")
+ # 执行表情包对象的删除方法
+ await emoji.delete()
+ # 从列表中移除该对象
+ self.emoji_objects.remove(emoji)
+ # 更新计数
+ self.emoji_num -= 1
+ removed_count += 1
+ continue
+
+ except Exception as item_error:
+ logger.error(f"[错误] 处理表情包记录时出错: {str(item_error)}")
+ continue
+
+ # 输出清理结果
+ if removed_count > 0:
+ logger.success(f"[清理] 已清理 {removed_count} 个失效的表情包记录")
+ logger.info(f"[统计] 清理前: {total_count} | 清理后: {len(self.emoji_objects)}")
+ else:
+ logger.info(f"[检查] 已检查 {total_count} 个表情包记录,全部完好")
+
+ except Exception as e:
+ logger.error(f"[错误] 检查表情包完整性失败: {str(e)}")
+ logger.error(traceback.format_exc())
+
+ async def start_periodic_check_register(self):
+ """定期检查表情包完整性和数量"""
+ await self.get_all_emoji_from_db()
+ while True:
+ logger.info("[扫描] 开始检查表情包完整性...")
+ await self.check_emoji_file_integrity()
+ await self.clear_temp_emoji()
+ logger.info("[扫描] 开始扫描新表情包...")
+
+ # 检查表情包目录是否存在
+ if not os.path.exists(EMOJI_DIR):
+ logger.warning(f"[警告] 表情包目录不存在: {EMOJI_DIR}")
+ os.makedirs(EMOJI_DIR, exist_ok=True)
+ logger.info(f"[创建] 已创建表情包目录: {EMOJI_DIR}")
+ await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
+ continue
+
+ # 检查目录是否为空
+ files = os.listdir(EMOJI_DIR)
+ if not files:
+ logger.warning(f"[警告] 表情包目录为空: {EMOJI_DIR}")
+ await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
+ continue
+
+ # 检查是否需要处理表情包(数量超过最大值或不足)
+ if (self.emoji_num > self.emoji_num_max and global_config.max_reach_deletion) or (
+ self.emoji_num < self.emoji_num_max
+ ):
+ try:
+ # 获取目录下所有图片文件
+ files_to_process = [
+ f
+ for f in files
+ if os.path.isfile(os.path.join(EMOJI_DIR, f))
+ and f.lower().endswith((".jpg", ".jpeg", ".png", ".gif"))
+ ]
+
+ # 处理每个符合条件的文件
+ for filename in files_to_process:
+ # 尝试注册表情包
+ success = await self.register_emoji_by_filename(filename)
+ if success:
+ # 注册成功则跳出循环
+ break
+ else:
+ # 注册失败则删除对应文件
+ file_path = os.path.join(EMOJI_DIR, filename)
+ os.remove(file_path)
+ logger.warning(f"[清理] 删除注册失败的表情包文件: {filename}")
+ except Exception as e:
+ logger.error(f"[错误] 扫描表情包目录失败: {str(e)}")
+
+ await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
+
+ async def get_all_emoji_from_db(self):
+ """获取所有表情包并初始化为MaiEmoji类对象
+
+ 参数:
+ hash: 可选,如果提供则只返回指定哈希值的表情包
+
+ 返回:
+ list[MaiEmoji]: 表情包对象列表
+ """
+ try:
+ self._ensure_db()
+
+ # 获取所有表情包
+ all_emoji_data = list(db.emoji.find())
+
+ # 将数据库记录转换为MaiEmoji对象
+ emoji_objects = []
+ for emoji_data in all_emoji_data:
+ emoji = MaiEmoji(
+ filename=emoji_data.get("filename", ""),
+ path=emoji_data.get("path", ""),
+ )
+
+ # 设置额外属性
+ emoji.hash = emoji_data.get("hash", "")
+ emoji.usage_count = emoji_data.get("usage_count", 0)
+ emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time()))
+ emoji.register_time = emoji_data.get("timestamp", time.time())
+ emoji.description = emoji_data.get("description", "")
+ emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载
+ emoji_objects.append(emoji)
+
+ # 存储到EmojiManager中
+ self.emoji_objects = emoji_objects
+
+ except Exception as e:
+ logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}")
+
+ async def get_emoji_from_db(self, hash=None):
+ """获取所有表情包并初始化为MaiEmoji类对象
+
+ 参数:
+ hash: 可选,如果提供则只返回指定哈希值的表情包
+
+ 返回:
+ list[MaiEmoji]: 表情包对象列表
+ """
+ try:
+ self._ensure_db()
+
+ # 准备查询条件
+ query = {}
+ if hash:
+ query = {"hash": hash}
+
+ # 获取所有表情包
+ all_emoji_data = list(db.emoji.find(query))
+
+ # 将数据库记录转换为MaiEmoji对象
+ emoji_objects = []
+ for emoji_data in all_emoji_data:
+ emoji = MaiEmoji(
+ filename=emoji_data.get("filename", ""),
+ path=emoji_data.get("path", ""),
+ )
+
+ # 设置额外属性
+ emoji.usage_count = emoji_data.get("usage_count", 0)
+ emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time()))
+ emoji.register_time = emoji_data.get("timestamp", time.time())
+ emoji.description = emoji_data.get("description", "")
+ emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载
+
+ emoji_objects.append(emoji)
+
+ # 存储到EmojiManager中
+ self.emoji_objects = emoji_objects
+
+ return emoji_objects
+
+ except Exception as e:
+ logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}")
+ return []
+
+ async def get_emoji_from_manager(self, hash) -> MaiEmoji:
+ """从EmojiManager中获取表情包
+
+ 参数:
+ hash:如果提供则只返回指定哈希值的表情包
+ """
+ for emoji in self.emoji_objects:
+ if emoji.hash == hash:
+ return emoji
+ return None
+
+ async def delete_emoji(self, emoji_hash: str) -> bool:
+ """根据哈希值删除表情包
+
+ Args:
+ emoji_hash: 表情包的哈希值
+
+ Returns:
+ bool: 是否成功删除
+ """
+ try:
+ self._ensure_db()
+
+ # 从emoji_objects中查找表情包对象
+ emoji = await self.get_emoji_from_manager(emoji_hash)
+
+ if not emoji:
+ logger.warning(f"[警告] 未找到哈希值为 {emoji_hash} 的表情包")
+ return False
+
+ # 使用MaiEmoji对象的delete方法删除表情包
+ success = await emoji.delete()
+
+ if success:
+ # 从emoji_objects列表中移除该对象
+ self.emoji_objects = [e for e in self.emoji_objects if e.hash != emoji_hash]
+ # 更新计数
+ self.emoji_num -= 1
+ logger.info(f"[统计] 当前表情包数量: {self.emoji_num}")
+
+ return True
+ else:
+ logger.error(f"[错误] 删除表情包失败: {emoji_hash}")
+ return False
+
+ except Exception as e:
+ logger.error(f"[错误] 删除表情包失败: {str(e)}")
+ logger.error(traceback.format_exc())
+ return False
+
+ def _emoji_objects_to_readable_list(self, emoji_objects):
+ """将表情包对象列表转换为可读的字符串列表
+
+ 参数:
+ emoji_objects: MaiEmoji对象列表
+
+ 返回:
+ list[str]: 可读的表情包信息字符串列表
+ """
+ emoji_info_list = []
+ for i, emoji in enumerate(emoji_objects):
+ # 转换时间戳为可读时间
+ time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(emoji.register_time))
+ # 构建每个表情包的信息字符串
+ emoji_info = (
+ f"编号: {i + 1}\n描述: {emoji.description}\n使用次数: {emoji.usage_count}\n添加时间: {time_str}\n"
+ )
+ emoji_info_list.append(emoji_info)
+ return emoji_info_list
+
+ async def replace_a_emoji(self, new_emoji: MaiEmoji):
+ """替换一个表情包
+
+ Args:
+ new_emoji: 新表情包对象
+
+ Returns:
+ bool: 是否成功替换表情包
+ """
+ try:
+ self._ensure_db()
+
+ # 获取所有表情包对象
+ all_emojis = self.emoji_objects
+
+ # 将表情包信息转换为可读的字符串
+ emoji_info_list = self._emoji_objects_to_readable_list(all_emojis)
+
+ # 构建提示词
+ prompt = (
+ f"{global_config.BOT_NICKNAME}的表情包存储已满({self.emoji_num}/{self.emoji_num_max}),"
+ f"需要决定是否删除一个旧表情包来为新表情包腾出空间。\n\n"
+ f"新表情包信息:\n"
+ f"描述: {new_emoji.description}\n\n"
+ f"现有表情包列表:\n" + "\n".join(emoji_info_list) + "\n\n"
+ "请决定:\n"
+ "1. 是否要删除某个现有表情包来为新表情包腾出空间?\n"
+ "2. 如果要删除,应该删除哪一个(给出编号)?\n"
+ "请只回答:'不删除'或'删除编号X'(X为表情包编号)。"
+ )
+
+ # 调用大模型进行决策
+ decision, _ = await self.llm_emotion_judge.generate_response_async(prompt, temperature=0.8)
+ logger.info(f"[决策] 大模型决策结果: {decision}")
+
+ # 解析决策结果
+ if "不删除" in decision:
+ logger.info("[决策] 决定不删除任何表情包")
+ return False
+
+ # 尝试从决策中提取表情包编号
+ match = re.search(r"删除编号(\d+)", decision)
+ if match:
+ emoji_index = int(match.group(1)) - 1 # 转换为0-based索引
+
+ # 检查索引是否有效
+ if 0 <= emoji_index < len(all_emojis):
+ emoji_to_delete = all_emojis[emoji_index]
+
+ # 删除选定的表情包
+ logger.info(f"[决策] 决定删除表情包: {emoji_to_delete.description}")
+ delete_success = await self.delete_emoji(emoji_to_delete.hash)
+
+ if delete_success:
+ # 修复:等待异步注册完成
+ register_success = await new_emoji.register_to_db()
+ if register_success:
+ self.emoji_objects.append(new_emoji)
+ self.emoji_num += 1
+ logger.success(f"[成功] 注册表情包: {new_emoji.description}")
+ return True
+ else:
+ logger.error(f"[错误] 注册表情包到数据库失败: {new_emoji.filename}")
+ return False
+ else:
+ logger.error("[错误] 删除表情包失败,无法完成替换")
+ return False
+ else:
+ logger.error(f"[错误] 无效的表情包编号: {emoji_index + 1}")
+ else:
+ logger.error(f"[错误] 无法从决策中提取表情包编号: {decision}")
+
+ return False
+
+ except Exception as e:
+ logger.error(f"[错误] 替换表情包失败: {str(e)}")
+ logger.error(traceback.format_exc())
+ return False
+
+ async def build_emoji_description(self, image_base64: str) -> Tuple[str, list]:
+ """获取表情包描述和情感列表
+
+ Args:
+ image_base64: 图片的base64编码
+
+ Returns:
+ Tuple[str, list]: 返回表情包描述和情感列表
+ """
+ try:
+ # 解码图片并获取格式
+ image_bytes = base64.b64decode(image_base64)
+ image_format = Image.open(io.BytesIO(image_bytes)).format.lower()
+
+ # 调用AI获取描述
+ if image_format == "gif" or image_format == "GIF":
+ image_base64 = image_manager.transform_gif(image_base64)
+ prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,详细描述一下表情包表达的情感和内容,请关注其幽默和讽刺意味"
+ description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, "jpg")
+ else:
+ prompt = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,请关注其幽默和讽刺意味"
+ description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format)
+
+ # 审核表情包
+ if global_config.EMOJI_CHECK:
+ prompt = f'''
+ 这是一个表情包,请对这个表情包进行审核,标准如下:
+ 1. 必须符合"{global_config.EMOJI_CHECK_PROMPT}"的要求
+ 2. 不能是色情、暴力、等违法违规内容,必须符合公序良俗
+ 3. 不能是任何形式的截图,聊天记录或视频截图
+ 4. 不要出现5个以上文字
+ 请回答这个表情包是否满足上述要求,是则回答是,否则回答否,不要出现任何其他内容
+ '''
+ content, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format)
+ if content == "否":
+ return None, []
+
+ # 分析情感含义
+ emotion_prompt = f"""
+ 基于这个表情包的描述:'{description}',请列出1-2个可能的情感标签,每个标签用一个词组表示,格式如下:
+ 幽默的讽刺
+ 悲伤的无奈
+ 愤怒的抗议
+ 愤怒的讽刺
+ 直接输出词组,词组检用逗号分隔。"""
+ emotions_text, _ = await self.llm_emotion_judge.generate_response_async(emotion_prompt, temperature=0.7)
+
+ # 处理情感列表
+ emotions = [e.strip() for e in emotions_text.split(",") if e.strip()]
+
+ return f"[表情包:{description}]", emotions
+
+ except Exception as e:
+ logger.error(f"获取表情包描述失败: {str(e)}")
+ return "", []
+
+ async def register_emoji_by_filename(self, filename: str) -> bool:
+ """读取指定文件名的表情包图片,分析并注册到数据库
+
+ Args:
+ filename: 表情包文件名,必须位于EMOJI_DIR目录下
+
+ Returns:
+ bool: 注册是否成功
+ """
+ try:
+ # 使用MaiEmoji类创建表情包实例
+ new_emoji = MaiEmoji(filename, EMOJI_DIR)
+ await new_emoji.initialize_hash_format()
+ emoji_base64 = image_path_to_base64(os.path.join(EMOJI_DIR, filename))
+ description, emotions = await self.build_emoji_description(emoji_base64)
+ if description == "":
+ return False
+ new_emoji.description = description
+ new_emoji.emotion = emotions
+
+ # 检查是否已经注册过
+ # 对比内存中是否存在相同哈希值的表情包
+ if await self.get_emoji_from_manager(new_emoji.hash):
+ logger.warning(f"[警告] 表情包已存在: {filename}")
+ return False
+
+ if self.emoji_num >= self.emoji_num_max:
+ logger.warning(f"表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max})")
+ replaced = await self.replace_a_emoji(new_emoji)
+ if not replaced:
+ logger.error("[错误] 替换表情包失败,无法完成注册")
+ return False
+ else:
+ # 修复:等待异步注册完成
+ register_success = await new_emoji.register_to_db()
+ if register_success:
+ self.emoji_objects.append(new_emoji)
+ self.emoji_num += 1
+ logger.success(f"[成功] 注册表情包: {filename}")
+ return True
+ else:
+ logger.error(f"[错误] 注册表情包到数据库失败: {filename}")
+ return False
+
+ except Exception as e:
+ logger.error(f"[错误] 注册表情包失败: {str(e)}")
+ logger.error(traceback.format_exc())
+ return False
+
+ async def clear_temp_emoji(self):
+ """每天清理临时表情包
+ 清理/data/emoji和/data/image目录下的所有文件
+ 当目录中文件数超过50时,会全部删除
+ """
+
+ logger.info("[清理] 开始清理临时表情包...")
+
+ # 清理emoji目录
+ emoji_dir = os.path.join(BASE_DIR, "emoji")
+ if os.path.exists(emoji_dir):
+ files = os.listdir(emoji_dir)
+ # 如果文件数超过50就全部删除
+ if len(files) > 50:
+ for filename in files:
+ file_path = os.path.join(emoji_dir, filename)
+ if os.path.isfile(file_path):
+ os.remove(file_path)
+ logger.debug(f"[清理] 删除表情包文件: {filename}")
+
+ # 清理image目录
+ image_dir = os.path.join(BASE_DIR, "image")
+ if os.path.exists(image_dir):
+ files = os.listdir(image_dir)
+ # 如果文件数超过50就全部删除
+ if len(files) > 50:
+ for filename in files:
+ file_path = os.path.join(image_dir, filename)
+ if os.path.isfile(file_path):
+ os.remove(file_path)
+ logger.debug(f"[清理] 删除图片文件: {filename}")
+
+ logger.success("[清理] 临时文件清理完成")
+
+
+# 创建全局单例
+emoji_manager = EmojiManager()
diff --git a/src/plugins/heartFC_chat/heartFC_Cycleinfo.py b/src/plugins/heartFC_chat/heartFC_Cycleinfo.py
new file mode 100644
index 000000000..966773841
--- /dev/null
+++ b/src/plugins/heartFC_chat/heartFC_Cycleinfo.py
@@ -0,0 +1,74 @@
+import time
+from typing import List, Optional, Dict, Any
+
+
+class CycleInfo:
+ """循环信息记录类"""
+
+ def __init__(self, cycle_id: int):
+ self.cycle_id = cycle_id
+ self.start_time = time.time()
+ self.end_time: Optional[float] = None
+ self.action_taken = False
+ self.action_type = "unknown"
+ self.reasoning = ""
+ self.timers: Dict[str, float] = {}
+ self.thinking_id = ""
+ self.replanned = False
+
+ # 添加响应信息相关字段
+ self.response_info: Dict[str, Any] = {
+ "response_text": [], # 回复的文本列表
+ "emoji_info": "", # 表情信息
+ "anchor_message_id": "", # 锚点消息ID
+ "reply_message_ids": [], # 回复消息ID列表
+ "sub_mind_thinking": "", # 子思维思考内容
+ }
+
+ def to_dict(self) -> Dict[str, Any]:
+ """将循环信息转换为字典格式"""
+ return {
+ "cycle_id": self.cycle_id,
+ "start_time": self.start_time,
+ "end_time": self.end_time,
+ "action_taken": self.action_taken,
+ "action_type": self.action_type,
+ "reasoning": self.reasoning,
+ "timers": self.timers,
+ "thinking_id": self.thinking_id,
+ "response_info": self.response_info,
+ }
+
+ def complete_cycle(self):
+ """完成循环,记录结束时间"""
+ self.end_time = time.time()
+
+ def set_action_info(self, action_type: str, reasoning: str, action_taken: bool):
+ """设置动作信息"""
+ self.action_type = action_type
+ self.reasoning = reasoning
+ self.action_taken = action_taken
+
+ def set_thinking_id(self, thinking_id: str):
+ """设置思考消息ID"""
+ self.thinking_id = thinking_id
+
+ def set_response_info(
+ self,
+ response_text: Optional[List[str]] = None,
+ emoji_info: Optional[str] = None,
+ anchor_message_id: Optional[str] = None,
+ reply_message_ids: Optional[List[str]] = None,
+ sub_mind_thinking: Optional[str] = None,
+ ):
+ """设置响应信息"""
+ if response_text is not None:
+ self.response_info["response_text"] = response_text
+ if emoji_info is not None:
+ self.response_info["emoji_info"] = emoji_info
+ if anchor_message_id is not None:
+ self.response_info["anchor_message_id"] = anchor_message_id
+ if reply_message_ids is not None:
+ self.response_info["reply_message_ids"] = reply_message_ids
+ if sub_mind_thinking is not None:
+ self.response_info["sub_mind_thinking"] = sub_mind_thinking
diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py
index 8735ff7df..903667b41 100644
--- a/src/plugins/heartFC_chat/heartFC_chat.py
+++ b/src/plugins/heartFC_chat/heartFC_chat.py
@@ -1,9 +1,11 @@
import asyncio
import time
import traceback
-from typing import List, Optional, Dict, Any
+import random # <-- 添加导入
+from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
+from collections import deque
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
-from src.plugins.chat.message import MessageSet, Seg # Local import needed after move
+from src.plugins.chat.message import Seg # Local import needed after move
from src.plugins.chat.chat_stream import ChatStream
from src.plugins.chat.message import UserInfo
from src.plugins.chat.chat_stream import chat_manager
@@ -11,16 +13,18 @@ from src.common.logger import get_module_logger, LogConfig, PFC_STYLE_CONFIG #
from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
-from src.plugins.utils.timer_calculater import Timer # <--- Import Timer
+from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
from src.plugins.heartFC_chat.heartFC_generator import HeartFCGenerator
from src.do_tool.tool_use import ToolUser
-from ..chat.message_sender import message_manager # <-- Import the global manager
-from src.plugins.chat.emoji_manager import emoji_manager
+from src.plugins.emoji_system.emoji_manager import emoji_manager
from src.plugins.utils.json_utils import process_llm_tool_response # 导入新的JSON工具
from src.heart_flow.sub_mind import SubMind
from src.heart_flow.observation import Observation
from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager
-# --- End import ---
+import contextlib
+from src.plugins.utils.chat_message_builder import num_new_messages_since
+from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
+from .heartFC_sender import HeartFCSender
INITIAL_DURATION = 60.0
@@ -34,31 +38,113 @@ interest_log_config = LogConfig(
logger = get_module_logger("HeartFCLoop", config=interest_log_config) # Logger Name Changed
-PLANNER_TOOL_DEFINITION = [
- {
- "type": "function",
- "function": {
- "name": "decide_reply_action",
- "description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。",
- "parameters": {
- "type": "object",
- "properties": {
- "action": {
- "type": "string",
- "enum": ["no_reply", "text_reply", "emoji_reply"],
- "description": "决定采取的行动:'no_reply'(不回复), 'text_reply'(文本回复, 可选附带表情) 或 'emoji_reply'(仅表情回复)。",
- },
- "reasoning": {"type": "string", "description": "做出此决定的简要理由。"},
- "emoji_query": {
- "type": "string",
- "description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。",
+# 默认动作定义
+DEFAULT_ACTIONS = {"no_reply": "不回复", "text_reply": "文本回复, 可选附带表情", "emoji_reply": "仅表情回复"}
+
+
+class ActionManager:
+ """动作管理器:控制每次决策可以使用的动作"""
+
+ def __init__(self):
+ # 初始化为默认动作集
+ self._available_actions: Dict[str, str] = DEFAULT_ACTIONS.copy()
+
+ def get_available_actions(self) -> Dict[str, str]:
+ """获取当前可用的动作集"""
+ return self._available_actions
+
+ def add_action(self, action_name: str, description: str) -> bool:
+ """
+ 添加新的动作
+
+ 参数:
+ action_name: 动作名称
+ description: 动作描述
+
+ 返回:
+ bool: 是否添加成功
+ """
+ if action_name in self._available_actions:
+ return False
+ self._available_actions[action_name] = description
+ return True
+
+ def remove_action(self, action_name: str) -> bool:
+ """
+ 移除指定动作
+
+ 参数:
+ action_name: 动作名称
+
+ 返回:
+ bool: 是否移除成功
+ """
+ if action_name not in self._available_actions:
+ return False
+ del self._available_actions[action_name]
+ return True
+
+ def clear_actions(self):
+ """清空所有动作"""
+ self._available_actions.clear()
+
+ def reset_to_default(self):
+ """重置为默认动作集"""
+ self._available_actions = DEFAULT_ACTIONS.copy()
+
+ def get_planner_tool_definition(self) -> List[Dict[str, Any]]:
+ """获取当前动作集对应的规划器工具定义"""
+ return [
+ {
+ "type": "function",
+ "function": {
+ "name": "decide_reply_action",
+ "description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string",
+ "enum": list(self._available_actions.keys()),
+ "description": "决定采取的行动:"
+ + ", ".join([f"'{k}'({v})" for k, v in self._available_actions.items()]),
+ },
+ "reasoning": {"type": "string", "description": "做出此决定的简要理由。"},
+ "emoji_query": {
+ "type": "string",
+ "description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。",
+ },
+ },
+ "required": ["action", "reasoning"],
},
},
- "required": ["action", "reasoning"],
- },
- },
- }
-]
+ }
+ ]
+
+
+# 在文件开头添加自定义异常类
+class HeartFCError(Exception):
+ """麦麦聊天系统基础异常类"""
+
+ pass
+
+
+class PlannerError(HeartFCError):
+ """规划器异常"""
+
+ pass
+
+
+class ReplierError(HeartFCError):
+ """回复器异常"""
+
+ pass
+
+
+class SenderError(HeartFCError):
+ """发送器异常"""
+
+ pass
class HeartFChatting:
@@ -68,18 +154,36 @@ class HeartFChatting:
其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。
"""
- def __init__(self, chat_id: str, sub_mind: SubMind, observations: Observation):
+ CONSECUTIVE_NO_REPLY_THRESHOLD = 5 # 连续不回复的阈值
+
+ def __init__(
+ self,
+ chat_id: str,
+ sub_mind: SubMind,
+ observations: Observation,
+ on_consecutive_no_reply_callback: Callable[[], Coroutine[None, None, None]],
+ ):
"""
HeartFChatting 初始化函数
参数:
chat_id: 聊天流唯一标识符(如stream_id)
+ sub_mind: 关联的子思维
+ observations: 关联的观察列表
+ on_consecutive_no_reply_callback: 连续不回复达到阈值时调用的异步回调函数
"""
# 基础属性
self.stream_id: str = chat_id # 聊天流ID
self.chat_stream: Optional[ChatStream] = None # 关联的聊天流
self.sub_mind: SubMind = sub_mind # 关联的子思维
- self.observations: Observation = observations # 关联的观察
+ self.observations: List[Observation] = observations # 关联的观察列表,用于监控聊天流状态
+ self.on_consecutive_no_reply_callback = on_consecutive_no_reply_callback
+
+ # 日志前缀
+ self.log_prefix: str = f"[{chat_manager.get_stream_name(chat_id) or chat_id}]"
+
+ # 动作管理器
+ self.action_manager = ActionManager()
# 初始化状态控制
self._initialized = False # 是否已初始化标志
@@ -88,11 +192,11 @@ class HeartFChatting:
# 依赖注入存储
self.gpt_instance = HeartFCGenerator() # 文本回复生成器
self.tool_user = ToolUser() # 工具使用实例
+ self.heart_fc_sender = HeartFCSender()
# LLM规划器配置
self.planner_llm = LLMRequest(
- model=global_config.llm_normal,
- temperature=global_config.llm_normal["temp"],
+ model=global_config.llm_plan,
max_tokens=1000,
request_type="action_planning", # 用于动作规划
)
@@ -101,331 +205,538 @@ class HeartFChatting:
self._loop_active: bool = False # 循环是否正在运行
self._loop_task: Optional[asyncio.Task] = None # 主循环任务
- def _get_log_prefix(self) -> str:
- """获取日志前缀,包含可读的流名称"""
- stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id
- return f"[{stream_name}]"
+ # 添加循环信息管理相关的属性
+ self._cycle_counter = 0
+ self._cycle_history: Deque[CycleInfo] = deque(maxlen=10) # 保留最近10个循环的信息
+ self._current_cycle: Optional[CycleInfo] = None
+ self._lian_xu_bu_hui_fu_ci_shu: int = 0 # <--- 新增:连续不回复计数器
+ self._shutting_down: bool = False # <--- 新增:关闭标志位
async def _initialize(self) -> bool:
"""
- 懒初始化以使用提供的标识符解析chat_stream和sub_hf。
+ 懒初始化以使用提供的标识符解析chat_stream。
确保实例已准备好处理触发器。
"""
if self._initialized:
return True
- log_prefix = self._get_log_prefix() # 获取前缀
- try:
- self.chat_stream = chat_manager.get_stream(self.stream_id)
- if not self.chat_stream:
- logger.error(f"{log_prefix} 获取ChatStream失败。")
- return False
- self._initialized = True
- logger.info(f"麦麦感觉到了,激发了HeartFChatting{log_prefix} 初始化成功。")
- return True
- except Exception as e:
- logger.error(f"{log_prefix} 初始化失败: {e}")
- logger.error(traceback.format_exc())
+ self.chat_stream = chat_manager.get_stream(self.stream_id)
+ if not self.chat_stream:
+ logger.error(f"{self.log_prefix} 获取ChatStream失败。")
return False
+ # 更新日志前缀(以防流名称发生变化)
+ self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]"
+
+ self._initialized = True
+ logger.info(f"麦麦感觉到了,可以开始认真水群{self.log_prefix} ")
+ return True
+
async def start(self):
"""
- 显式尝试启动 HeartFChatting 的主循环。
- 如果循环未激活,则启动循环。
+ 启动 HeartFChatting 的主循环。
+ 注意:调用此方法前必须确保已经成功初始化。
"""
- log_prefix = self._get_log_prefix()
- if not self._initialized:
- if not await self._initialize():
- logger.error(f"{log_prefix} 无法启动循环: 初始化失败。")
- return
- logger.info(f"{log_prefix} 尝试显式启动循环...")
+ logger.info(f"{self.log_prefix} 开始认真水群(HFC)...")
await self._start_loop_if_needed()
async def _start_loop_if_needed(self):
"""检查是否需要启动主循环,如果未激活则启动。"""
- log_prefix = self._get_log_prefix()
- should_start_loop = False
- # 直接检查是否激活,无需检查计时器
- if not self._loop_active:
- should_start_loop = True
- self._loop_active = True # 标记为活动,防止重复启动
+ # 如果循环已经激活,直接返回
+ if self._loop_active:
+ return
- if should_start_loop:
- # 检查是否已有任务在运行(理论上不应该,因为 _loop_active=False)
- if self._loop_task and not self._loop_task.done():
- logger.warning(f"{log_prefix} 发现之前的循环任务仍在运行(不符合预期)。取消旧任务。")
- self._loop_task.cancel()
- try:
- # 等待旧任务确实被取消
- await asyncio.wait_for(self._loop_task, timeout=0.5)
- except (asyncio.CancelledError, asyncio.TimeoutError):
- pass # 忽略取消或超时错误
- self._loop_task = None # 清理旧任务引用
+ # 标记为活动状态,防止重复启动
+ self._loop_active = True
- logger.info(f"{log_prefix} 循环未激活,启动主循环...")
- # 创建新的循环任务
- self._loop_task = asyncio.create_task(self._run_pf_loop())
- # 添加完成回调
- self._loop_task.add_done_callback(self._handle_loop_completion)
- # else:
- # logger.trace(f"{log_prefix} 不需要启动循环(已激活)") # 可以取消注释以进行调试
+ # 检查是否已有任务在运行(理论上不应该,因为 _loop_active=False)
+ if self._loop_task and not self._loop_task.done():
+ logger.warning(f"{self.log_prefix} 发现之前的循环任务仍在运行(不符合预期)。取消旧任务。")
+ self._loop_task.cancel()
+ try:
+ # 等待旧任务确实被取消
+ await asyncio.wait_for(self._loop_task, timeout=0.5)
+ except (asyncio.CancelledError, asyncio.TimeoutError):
+ pass # 忽略取消或超时错误
+ self._loop_task = None # 清理旧任务引用
+
+ logger.info(f"{self.log_prefix} 启动认真水群(HFC)主循环...")
+ # 创建新的循环任务
+ self._loop_task = asyncio.create_task(self._hfc_loop())
+ # 添加完成回调
+ self._loop_task.add_done_callback(self._handle_loop_completion)
def _handle_loop_completion(self, task: asyncio.Task):
- """当 _run_pf_loop 任务完成时执行的回调。"""
- log_prefix = self._get_log_prefix()
+ """当 _hfc_loop 任务完成时执行的回调。"""
try:
exception = task.exception()
if exception:
- logger.error(f"{log_prefix} HeartFChatting: 麦麦脱离了聊天(异常): {exception}")
+ logger.error(f"{self.log_prefix} HeartFChatting: 麦麦脱离了聊天(异常): {exception}")
logger.error(traceback.format_exc()) # Log full traceback for exceptions
else:
# Loop completing normally now means it was cancelled/shutdown externally
- logger.info(f"{log_prefix} HeartFChatting: 麦麦脱离了聊天 (外部停止)")
+ logger.info(f"{self.log_prefix} HeartFChatting: 麦麦脱离了聊天 (外部停止)")
except asyncio.CancelledError:
- logger.info(f"{log_prefix} HeartFChatting: 麦麦脱离了聊天(任务取消)")
+ logger.info(f"{self.log_prefix} HeartFChatting: 麦麦脱离了聊天(任务取消)")
finally:
self._loop_active = False
self._loop_task = None
if self._processing_lock.locked():
- logger.warning(f"{log_prefix} HeartFChatting: 处理锁在循环结束时仍被锁定,强制释放。")
+ logger.warning(f"{self.log_prefix} HeartFChatting: 处理锁在循环结束时仍被锁定,强制释放。")
self._processing_lock.release()
- async def _run_pf_loop(self):
- """
- 主循环,持续进行计划并可能回复消息,直到被外部取消。
- 管理每个循环周期的处理锁。
- """
- log_prefix = self._get_log_prefix()
- logger.info(f"{log_prefix} HeartFChatting: 麦麦打算好好聊聊 (进入专注模式)")
+ async def _hfc_loop(self):
+ """主循环,持续进行计划并可能回复消息,直到被外部取消。"""
try:
- thinking_id = ""
- while True: # Loop indefinitely until cancelled
- cycle_timers = {} # <--- Initialize timers dict for this cycle
+ while True: # 主循环
+ # --- 在循环开始处检查关闭标志 ---
+ if self._shutting_down:
+ logger.info(f"{self.log_prefix} 检测到关闭标志,退出 HFC 循环。")
+ break
+ # --------------------------------
- # Access MessageManager directly
- if message_manager.check_if_sending_message_exist(self.stream_id, thinking_id):
- # logger.info(f"{log_prefix} HeartFChatting: 麦麦还在发消息,等会再规划")
- await asyncio.sleep(1)
- continue
- else:
- # logger.info(f"{log_prefix} HeartFChatting: 麦麦不发消息了,开始规划")
- pass
+ # 创建新的循环信息
+ self._cycle_counter += 1
+ self._current_cycle = CycleInfo(self._cycle_counter)
- # 记录循环周期开始时间,用于计时和休眠计算
+ # 初始化周期状态
+ cycle_timers = {}
loop_cycle_start_time = time.monotonic()
- action_taken_this_cycle = False
- acquired_lock = False
- planner_start_db_time = 0.0 # 初始化
- try:
- with Timer("Total Cycle", cycle_timers) as _total_timer: # <--- Start total cycle timer
- # Use try_acquire pattern or timeout?
- await self._processing_lock.acquire()
- acquired_lock = True
- # logger.debug(f"{log_prefix} HeartFChatting: 循环获取到处理锁")
+ # 执行规划和处理阶段
+ async with self._get_cycle_context() as acquired_lock:
+ if not acquired_lock:
+ # 如果未能获取锁(理论上不太可能,除非 shutdown 过程中释放了但又被抢了?)
+ # 或者也可以在这里再次检查 self._shutting_down
+ if self._shutting_down:
+ break # 再次检查,确保退出
+ logger.warning(f"{self.log_prefix} 未能获取循环处理锁,跳过本次循环。")
+ await asyncio.sleep(0.1) # 短暂等待避免空转
+ continue
- # 在规划前记录数据库时间戳
- planner_start_db_time = time.time()
+ # 记录规划开始时间点
+ planner_start_db_time = time.time()
- # --- Planner --- #
- planner_result = {}
- with Timer("Planner", cycle_timers): # <--- Start Planner timer
- planner_result = await self._planner()
- action = planner_result.get("action", "error")
- reasoning = planner_result.get("reasoning", "Planner did not provide reasoning.")
- emoji_query = planner_result.get("emoji_query", "")
- llm_error = planner_result.get("llm_error", False)
+ # 主循环:思考->决策->执行
+ action_taken, thinking_id = await self._think_plan_execute_loop(cycle_timers, planner_start_db_time)
- if llm_error:
- logger.error(f"{log_prefix} Planner LLM 失败,跳过本周期回复尝试。理由: {reasoning}")
- # Optionally add a longer sleep?
- action_taken_this_cycle = False # Ensure no action is counted
- # Continue to sleep logic
+ # 更新循环信息
+ self._current_cycle.set_thinking_id(thinking_id)
+ self._current_cycle.timers = cycle_timers
- elif action == "text_reply":
- logger.debug(f"{log_prefix} HeartFChatting: 麦麦决定回复文本. 理由: {reasoning}")
- action_taken_this_cycle = True
- anchor_message = await self._get_anchor_message()
- if not anchor_message:
- logger.error(f"{log_prefix} 循环: 无法获取锚点消息用于回复. 跳过周期.")
- else:
- # --- Create Thinking Message (Moved) ---
- thinking_id = await self._create_thinking_message(anchor_message)
- if not thinking_id:
- logger.error(f"{log_prefix} 循环: 无法创建思考ID. 跳过周期.")
- else:
- replier_result = None
- try:
- # --- Replier Work --- #
- with Timer("Replier", cycle_timers): # <--- Start Replier timer
- replier_result = await self._replier_work(
- anchor_message=anchor_message,
- thinking_id=thinking_id,
- reason=reasoning,
- )
- except Exception as e_replier:
- logger.error(f"{log_prefix} 循环: 回复器工作失败: {e_replier}")
- # self._cleanup_thinking_message(thinking_id) <-- Remove cleanup call
+ # 防止循环过快消耗资源
+ await self._handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix)
- if replier_result:
- # --- Sender Work --- #
- try:
- with Timer("Sender", cycle_timers): # <--- Start Sender timer
- await self._sender(
- thinking_id=thinking_id,
- anchor_message=anchor_message,
- response_set=replier_result,
- send_emoji=emoji_query,
- )
- # logger.info(f"{log_prefix} 循环: 发送器完成成功.")
- except Exception as e_sender:
- logger.error(f"{log_prefix} 循环: 发送器失败: {e_sender}")
- # _sender should handle cleanup, but double check
- # self._cleanup_thinking_message(thinking_id) <-- Remove cleanup call
- else:
- logger.warning(f"{log_prefix} 循环: 回复器未产生结果. 跳过发送.")
- # self._cleanup_thinking_message(thinking_id) <-- Remove cleanup call
- elif action == "emoji_reply":
- logger.info(
- f"{log_prefix} HeartFChatting: 麦麦决定回复表情 ('{emoji_query}'). 理由: {reasoning}"
- )
- action_taken_this_cycle = True
- anchor = await self._get_anchor_message()
- if anchor:
- try:
- # --- Handle Emoji (Moved) --- #
- with Timer("Emoji Handler", cycle_timers): # <--- Start Emoji timer
- await self._handle_emoji(anchor, [], emoji_query)
- except Exception as e_emoji:
- logger.error(f"{log_prefix} 循环: 发送表情失败: {e_emoji}")
- else:
- logger.warning(f"{log_prefix} 循环: 无法发送表情, 无法获取锚点.")
- action_taken_this_cycle = True # 即使发送失败,Planner 也决策了动作
+ # 完成当前循环并保存历史
+ self._current_cycle.complete_cycle()
+ self._cycle_history.append(self._current_cycle)
- elif action == "no_reply":
- logger.info(f"{log_prefix} HeartFChatting: 麦麦决定不回复. 原因: {reasoning}")
- action_taken_this_cycle = False # 标记为未执行动作
- # --- 新增:等待新消息 ---
- logger.debug(f"{log_prefix} HeartFChatting: 开始等待新消息 (自 {planner_start_db_time})...")
- observation = None
+ # 记录循环信息和计时器结果
+ timer_strings = []
+ for name, elapsed in cycle_timers.items():
+ formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒"
+ timer_strings.append(f"{name}: {formatted_time}")
- observation = self.observations[0]
-
- if observation:
- with Timer("Wait New Msg", cycle_timers): # <--- Start Wait timer
- wait_start_time = time.monotonic()
- while True:
- # 检查是否有新消息
- has_new = await observation.has_new_messages_since(planner_start_db_time)
- if has_new:
- logger.info(f"{log_prefix} HeartFChatting: 检测到新消息,结束等待。")
- break # 收到新消息,退出等待
-
- # 检查等待是否超时(例如,防止无限等待)
- if time.monotonic() - wait_start_time > 60: # 等待60秒示例
- logger.warning(f"{log_prefix} HeartFChatting: 等待新消息超时(60秒)。")
- break # 超时退出
-
- # 等待一段时间再检查
- try:
- await asyncio.sleep(1.5) # 检查间隔
- except asyncio.CancelledError:
- logger.info(f"{log_prefix} 等待新消息的 sleep 被中断。")
- raise # 重新抛出取消错误,以便外层循环处理
- else:
- logger.warning(
- f"{log_prefix} HeartFChatting: 无法获取 Observation 实例,无法等待新消息。"
- )
- # --- 等待结束 ---
-
- elif action == "error": # Action specifically set to error by planner
- logger.error(f"{log_prefix} HeartFChatting: Planner返回错误状态. 原因: {reasoning}")
- action_taken_this_cycle = False
-
- else: # Unknown action from planner
- logger.warning(
- f"{log_prefix} HeartFChatting: Planner返回未知动作 '{action}'. 原因: {reasoning}"
- )
- action_taken_this_cycle = False
-
- # --- Print Timer Results --- #
- if cycle_timers: # 先检查cycle_timers是否非空
- timer_strings = []
- for name, elapsed in cycle_timers.items():
- # 直接格式化存储在字典中的浮点数 elapsed
- formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒"
- timer_strings.append(f"{name}: {formatted_time}")
-
- if timer_strings: # 如果有有效计时器数据才打印
- logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}")
-
- # --- Timer Decrement Removed --- #
- cycle_duration = time.monotonic() - loop_cycle_start_time
-
- except Exception as e_cycle:
- logger.error(f"{log_prefix} 循环周期执行时发生错误: {e_cycle}")
- logger.error(traceback.format_exc())
- if acquired_lock and self._processing_lock.locked():
- self._processing_lock.release()
- acquired_lock = False
- logger.warning(f"{log_prefix} 由于循环周期中的错误释放了处理锁.")
-
- finally:
- if acquired_lock:
- self._processing_lock.release()
- # logger.trace(f"{log_prefix} 循环释放了处理锁.") # Reduce noise
-
- if cycle_duration > 0.1:
- logger.debug(f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s.")
-
- # --- Delay --- #
- try:
- sleep_duration = 0.0
- if not action_taken_this_cycle and cycle_duration < 1.5:
- sleep_duration = 1.5 - cycle_duration
- elif cycle_duration < 0.2: # Keep minimal sleep even after action
- sleep_duration = 0.2
-
- if sleep_duration > 0:
- # logger.debug(f"{log_prefix} Sleeping for {sleep_duration:.2f}s")
- await asyncio.sleep(sleep_duration)
-
- except asyncio.CancelledError:
- logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.")
- break # Exit loop immediately on cancellation
+ logger.debug(
+ f"{self.log_prefix} 第 #{self._current_cycle.cycle_id}次思考完成,"
+ f"耗时: {self._current_cycle.end_time - self._current_cycle.start_time:.2f}秒, "
+ f"动作: {self._current_cycle.action_type}"
+ + (f"\n计时器详情: {'; '.join(timer_strings)}" if timer_strings else "")
+ )
except asyncio.CancelledError:
- logger.info(f"{log_prefix} HeartFChatting: 麦麦的聊天主循环被取消了")
- except Exception as e_loop_outer:
- logger.error(f"{log_prefix} HeartFChatting: 麦麦的聊天主循环意外出错: {e_loop_outer}")
+ # 设置了关闭标志位后被取消是正常流程
+ if not self._shutting_down:
+ logger.warning(f"{self.log_prefix} HeartFChatting: 麦麦的认真水群(HFC)循环意外被取消")
+ else:
+ logger.info(f"{self.log_prefix} HeartFChatting: 麦麦的认真水群(HFC)循环已取消 (正常关闭)")
+ except Exception as e:
+ logger.error(f"{self.log_prefix} HeartFChatting: 意外错误: {e}")
logger.error(traceback.format_exc())
- finally:
- # State reset is primarily handled by _handle_loop_completion callback
- logger.info(f"{log_prefix} HeartFChatting: 麦麦的聊天主循环结束。")
- async def _planner(self) -> Dict[str, Any]:
+ @contextlib.asynccontextmanager
+ async def _get_cycle_context(self):
+ """
+ 循环周期的上下文管理器
+
+ 用于确保资源的正确获取和释放:
+ 1. 获取处理锁
+ 2. 执行操作
+ 3. 释放锁
+ """
+ acquired = False
+ try:
+ await self._processing_lock.acquire()
+ acquired = True
+ yield acquired
+ finally:
+ if acquired and self._processing_lock.locked():
+ self._processing_lock.release()
+
+ async def _check_new_messages(self, start_time: float) -> bool:
+ """
+ 检查从指定时间点后是否有新消息
+
+ 参数:
+ start_time: 开始检查的时间点
+
+ 返回:
+ bool: 是否有新消息
+ """
+ try:
+ new_msg_count = num_new_messages_since(self.stream_id, start_time)
+ if new_msg_count > 0:
+ logger.info(f"{self.log_prefix} 检测到{new_msg_count}条新消息")
+ return True
+ return False
+ except Exception as e:
+ logger.error(f"{self.log_prefix} 检查新消息时出错: {e}")
+ return False
+
+ async def _think_plan_execute_loop(self, cycle_timers: dict, planner_start_db_time: float) -> tuple[bool, str]:
+ """执行规划阶段"""
+ try:
+ # think:思考
+ current_mind = await self._get_submind_thinking(cycle_timers)
+ # 记录子思维思考内容
+ if self._current_cycle:
+ self._current_cycle.set_response_info(sub_mind_thinking=current_mind)
+
+ # plan:决策
+ with Timer("决策", cycle_timers):
+ planner_result = await self._planner(current_mind, cycle_timers)
+
+ action = planner_result.get("action", "error")
+ reasoning = planner_result.get("reasoning", "未提供理由")
+
+ self._current_cycle.set_action_info(action, reasoning, False)
+
+ # 在获取规划结果后检查新消息
+ if await self._check_new_messages(planner_start_db_time):
+ if random.random() < 0.3:
+ logger.info(f"{self.log_prefix} 看到了新消息,麦麦决定重新观察和规划...")
+ # 重新规划
+ with Timer("重新决策", cycle_timers):
+ self._current_cycle.replanned = True
+ planner_result = await self._planner(current_mind, cycle_timers, is_re_planned=True)
+ logger.info(f"{self.log_prefix} 重新规划完成.")
+
+ # 解析规划结果
+ action = planner_result.get("action", "error")
+ reasoning = planner_result.get("reasoning", "未提供理由")
+ # 更新循环信息
+ self._current_cycle.set_action_info(action, reasoning, True)
+
+ # 处理LLM错误
+ if planner_result.get("llm_error"):
+ logger.error(f"{self.log_prefix} LLM失败: {reasoning}")
+ return False, ""
+
+ # execute:执行
+
+ return await self._handle_action(
+ action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time
+ )
+
+ except PlannerError as e:
+ logger.error(f"{self.log_prefix} 规划错误: {e}")
+ # 更新循环信息
+ self._current_cycle.set_action_info("error", str(e), False)
+ return False, ""
+
+ async def _handle_action(
+ self, action: str, reasoning: str, emoji_query: str, cycle_timers: dict, planner_start_db_time: float
+ ) -> tuple[bool, str]:
+ """
+ 处理规划动作
+
+ 参数:
+ action: 动作类型
+ reasoning: 决策理由
+ emoji_query: 表情查询
+ cycle_timers: 计时器字典
+ planner_start_db_time: 规划开始时间
+
+ 返回:
+ tuple[bool, str]: (是否执行了动作, 思考消息ID)
+ """
+ action_handlers = {
+ "text_reply": self._handle_text_reply,
+ "emoji_reply": self._handle_emoji_reply,
+ "no_reply": self._handle_no_reply,
+ }
+
+ handler = action_handlers.get(action)
+ if not handler:
+ logger.warning(f"{self.log_prefix} 未知动作: {action}, 原因: {reasoning}")
+ return False, ""
+
+ try:
+ if action == "text_reply":
+ return await handler(reasoning, emoji_query, cycle_timers)
+ elif action == "emoji_reply":
+ return await handler(reasoning, emoji_query), ""
+ else: # no_reply
+ return await handler(reasoning, planner_start_db_time, cycle_timers), ""
+ except HeartFCError as e:
+ logger.error(f"{self.log_prefix} 处理{action}时出错: {e}")
+ # 出错时也重置计数器
+ self._lian_xu_bu_hui_fu_ci_shu = 0
+ return False, ""
+
+ async def _handle_text_reply(self, reasoning: str, emoji_query: str, cycle_timers: dict) -> tuple[bool, str]:
+ """
+ 处理文本回复
+
+ 工作流程:
+ 1. 获取锚点消息
+ 2. 创建思考消息
+ 3. 生成回复
+ 4. 发送消息
+
+ 参数:
+ reasoning: 回复原因
+ emoji_query: 表情查询
+ cycle_timers: 计时器字典
+
+ 返回:
+ tuple[bool, str]: (是否回复成功, 思考消息ID)
+ """
+ # 重置连续不回复计数器
+ self._lian_xu_bu_hui_fu_ci_shu = 0
+
+ # 获取锚点消息
+ anchor_message = await self._get_anchor_message()
+ if not anchor_message:
+ raise PlannerError("无法获取锚点消息")
+
+ # 创建思考消息
+ thinking_id = await self._create_thinking_message(anchor_message)
+ if not thinking_id:
+ raise PlannerError("无法创建思考消息")
+
+ try:
+ # 生成回复
+ with Timer("生成回复", cycle_timers):
+ reply = await self._replier_work(
+ anchor_message=anchor_message,
+ thinking_id=thinking_id,
+ reason=reasoning,
+ )
+
+ if not reply:
+ raise ReplierError("回复生成失败")
+
+ # 发送消息
+
+ with Timer("发送消息", cycle_timers):
+ await self._sender(
+ thinking_id=thinking_id,
+ anchor_message=anchor_message,
+ response_set=reply,
+ send_emoji=emoji_query,
+ )
+
+ return True, thinking_id
+
+ except (ReplierError, SenderError) as e:
+ logger.error(f"{self.log_prefix} 回复失败: {e}")
+ return True, thinking_id # 仍然返回thinking_id以便跟踪
+
+ async def _handle_emoji_reply(self, reasoning: str, emoji_query: str) -> bool:
+ """
+ 处理表情回复
+
+ 工作流程:
+ 1. 获取锚点消息
+ 2. 发送表情
+
+ 参数:
+ reasoning: 回复原因
+ emoji_query: 表情查询
+
+ 返回:
+ bool: 是否发送成功
+ """
+ logger.info(f"{self.log_prefix} 决定回复表情({emoji_query}): {reasoning}")
+
+ try:
+ anchor = await self._get_anchor_message()
+ if not anchor:
+ raise PlannerError("无法获取锚点消息")
+
+ await self._handle_emoji(anchor, [], emoji_query)
+ return True
+
+ except Exception as e:
+ logger.error(f"{self.log_prefix} 表情发送失败: {e}")
+ return False
+
+ async def _handle_no_reply(self, reasoning: str, planner_start_db_time: float, cycle_timers: dict) -> bool:
+ """
+ 处理不回复的情况
+
+ 工作流程:
+ 1. 等待新消息、超时或关闭信号
+ 2. 根据等待结果更新连续不回复计数
+ 3. 如果达到阈值,触发回调
+
+ 参数:
+ reasoning: 不回复的原因
+ planner_start_db_time: 规划开始时间
+ cycle_timers: 计时器字典
+
+ 返回:
+ bool: 是否成功处理
+ """
+ logger.info(f"{self.log_prefix} 决定不回复: {reasoning}")
+
+ observation = self.observations[0] if self.observations else None
+
+ try:
+ with Timer("等待新消息", cycle_timers):
+ # 等待新消息、超时或关闭信号,并获取结果
+ await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix)
+
+ if not self._shutting_down:
+ self._lian_xu_bu_hui_fu_ci_shu += 1
+ logger.debug(
+ f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{self.CONSECUTIVE_NO_REPLY_THRESHOLD}"
+ )
+
+ # 检查是否达到阈值
+ if self._lian_xu_bu_hui_fu_ci_shu >= self.CONSECUTIVE_NO_REPLY_THRESHOLD:
+ logger.info(
+ f"{self.log_prefix} 连续不回复达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次),调用回调请求状态转换"
+ )
+ # 调用回调。注意:这里不重置计数器,依赖回调函数成功改变状态来隐式重置上下文。
+ await self.on_consecutive_no_reply_callback()
+
+ return True
+
+ except asyncio.CancelledError:
+ # 如果在等待过程中任务被取消(可能是因为 shutdown)
+ logger.info(f"{self.log_prefix} 处理 'no_reply' 时等待被中断 (CancelledError)")
+ # 让异常向上传播,由 _hfc_loop 的异常处理逻辑接管
+ raise
+ except Exception as e: # 捕获调用管理器或其他地方可能发生的错误
+ logger.error(f"{self.log_prefix} 处理 'no_reply' 时发生错误: {e}")
+ logger.error(traceback.format_exc())
+ # 发生意外错误时,可以选择是否重置计数器,这里选择不重置
+ return False # 表示动作未成功
+
+ async def _wait_for_new_message(self, observation, planner_start_db_time: float, log_prefix: str) -> bool:
+ """
+ 等待新消息 或 检测到关闭信号
+
+ 参数:
+ observation: 观察实例
+ planner_start_db_time: 开始等待的时间
+ log_prefix: 日志前缀
+
+ 返回:
+ bool: 是否检测到新消息 (如果因关闭信号退出则返回 False)
+ """
+ wait_start_time = time.monotonic()
+ while True:
+ # --- 在每次循环开始时检查关闭标志 ---
+ if self._shutting_down:
+ logger.info(f"{log_prefix} 等待新消息时检测到关闭信号,中断等待。")
+ return False # 表示因为关闭而退出
+ # -----------------------------------
+
+ # 检查新消息
+ if await observation.has_new_messages_since(planner_start_db_time):
+ logger.info(f"{log_prefix} 检测到新消息")
+ return True
+
+ # 检查超时 (放在检查新消息和关闭之后)
+ if time.monotonic() - wait_start_time > 120:
+ logger.warning(f"{log_prefix} 等待新消息超时(20秒)")
+ return False
+
+ try:
+ # 短暂休眠,让其他任务有机会运行,并能更快响应取消或关闭
+ await asyncio.sleep(0.5) # 缩短休眠时间
+ except asyncio.CancelledError:
+ # 如果在休眠时被取消,再次检查关闭标志
+ # 如果是正常关闭,则不需要警告
+ if not self._shutting_down:
+ logger.warning(f"{log_prefix} _wait_for_new_message 的休眠被意外取消")
+ # 无论如何,重新抛出异常,让上层处理
+ raise
+
+ async def _log_cycle_timers(self, cycle_timers: dict, log_prefix: str):
+ """记录循环周期的计时器结果"""
+ if cycle_timers:
+ timer_strings = []
+ for name, elapsed in cycle_timers.items():
+ formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒"
+ timer_strings.append(f"{name}: {formatted_time}")
+
+ if timer_strings:
+ # 在记录前检查关闭标志
+ if not self._shutting_down:
+ logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}")
+
+ async def _handle_cycle_delay(self, action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str):
+ """处理循环延迟"""
+ cycle_duration = time.monotonic() - cycle_start_time
+
+ try:
+ sleep_duration = 0.0
+ if not action_taken_this_cycle and cycle_duration < 1:
+ sleep_duration = 1 - cycle_duration
+ elif cycle_duration < 0.2:
+ sleep_duration = 0.2
+
+ if sleep_duration > 0:
+ await asyncio.sleep(sleep_duration)
+
+ except asyncio.CancelledError:
+ logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.")
+ raise
+
+ async def _get_submind_thinking(self, cycle_timers: dict) -> str:
+ """
+ 获取子思维的思考结果
+
+ 返回:
+ str: 思考结果,如果思考失败则返回错误信息
+ """
+ try:
+ with Timer("观察", cycle_timers):
+ observation = self.observations[0]
+ await observation.observe()
+
+ # 获取上一个循环的信息
+ last_cycle = self._cycle_history[-1] if self._cycle_history else None
+
+ with Timer("思考", cycle_timers):
+ # 获取上一个循环的动作
+ # 传递上一个循环的信息给 do_thinking_before_reply
+ current_mind, _past_mind = await self.sub_mind.do_thinking_before_reply(last_cycle=last_cycle)
+ return current_mind
+ except Exception as e:
+ logger.error(f"{self.log_prefix}[SubMind] 思考失败: {e}")
+ logger.error(traceback.format_exc())
+ return "[思考时出错]"
+
+ async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]:
"""
规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。
+
+ 参数:
+ current_mind: 子思维的当前思考结果
"""
- log_prefix = self._get_log_prefix()
- observed_messages: List[dict] = []
+ logger.info(f"{self.log_prefix}[Planner] 开始{'重新' if is_re_planned else ''}执行规划器")
- current_mind: Optional[str] = None
- llm_error = False
-
- try:
- observation = self.observations[0]
+ # 获取观察信息
+ observation = self.observations[0]
+ if is_re_planned:
await observation.observe()
- observed_messages = observation.talking_message
- observed_messages_str = observation.talking_message_str
- except Exception as e:
- logger.error(f"{log_prefix}[Planner] 获取观察信息时出错: {e}")
-
- try:
- current_mind, _past_mind = await self.sub_mind.do_thinking_before_reply()
- except Exception as e_subhf:
- logger.error(f"{log_prefix}[Planner] SubHeartflow 思考失败: {e_subhf}")
- current_mind = "[思考时出错]"
+ observed_messages = observation.talking_message
+ observed_messages_str = observation.talking_message_str
# --- 使用 LLM 进行决策 --- #
action = "no_reply" # 默认动作
@@ -434,23 +745,33 @@ class HeartFChatting:
llm_error = False # LLM错误标志
try:
+ # 构建提示词
+
+ if is_re_planned:
+ replan_prompt = await self._build_replan_prompt(
+ self._current_cycle.action_type, self._current_cycle.reasoning
+ )
+ prompt = replan_prompt
+ else:
+ replan_prompt = ""
prompt = await self._build_planner_prompt(
- observed_messages_str, current_mind, self.sub_mind.structured_info
+ observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt
)
payload = {
- "model": self.planner_llm.model_name,
+ "model": global_config.llm_plan["name"],
"messages": [{"role": "user", "content": prompt}],
- "tools": PLANNER_TOOL_DEFINITION,
+ "tools": self.action_manager.get_planner_tool_definition(),
"tool_choice": {"type": "function", "function": {"name": "decide_reply_action"}},
}
# 执行LLM请求
+
try:
response = await self.planner_llm._execute_request(
endpoint="/chat/completions", payload=payload, prompt=prompt
)
except Exception as req_e:
- logger.error(f"{log_prefix}[Planner] LLM请求执行失败: {req_e}")
+ logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}")
return {
"action": "error",
"reasoning": f"LLM请求执行失败: {req_e}",
@@ -460,28 +781,40 @@ class HeartFChatting:
"llm_error": True,
}
- # 使用辅助函数处理工具调用响应
- success, arguments, error_msg = process_llm_tool_response(
- response, expected_tool_name="decide_reply_action", log_prefix=f"{log_prefix}[Planner] "
- )
+ # 处理LLM响应
+ with Timer("使用工具", cycle_timers):
+ # 使用辅助函数处理工具调用响应
+ success, arguments, error_msg = process_llm_tool_response(
+ response, expected_tool_name="decide_reply_action", log_prefix=f"{self.log_prefix}[Planner] "
+ )
- if success:
- # 提取决策参数
- action = arguments.get("action", "no_reply")
- reasoning = arguments.get("reasoning", "未提供理由")
- emoji_query = arguments.get("emoji_query", "")
+ if success:
+ # 提取决策参数
+ action = arguments.get("action", "no_reply")
+ # 验证动作是否在可用动作集中
+ if action not in self.action_manager.get_available_actions():
+ logger.warning(
+ f"{self.log_prefix}[Planner] LLM返回了未授权的动作: {action},使用默认动作no_reply"
+ )
+ action = "no_reply"
+ reasoning = f"LLM返回了未授权的动作: {action}"
+ else:
+ reasoning = arguments.get("reasoning", "未提供理由")
+ emoji_query = arguments.get("emoji_query", "")
- # 记录决策结果
- logger.debug(f"{log_prefix}[Planner] 决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'")
- else:
- # 处理工具调用失败
- logger.warning(f"{log_prefix}[Planner] {error_msg}")
- action = "error"
- reasoning = error_msg
- llm_error = True
+ # 记录决策结果
+ logger.debug(
+ f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
+ )
+ else:
+ # 处理工具调用失败
+ logger.warning(f"{self.log_prefix}[Planner] {error_msg}")
+ action = "error"
+ reasoning = error_msg
+ llm_error = True
except Exception as llm_e:
- logger.error(f"{log_prefix}[Planner] Planner LLM处理过程中出错: {llm_e}")
+ logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中出错: {llm_e}")
logger.error(traceback.format_exc()) # 记录完整堆栈以便调试
action = "error"
reasoning = f"LLM处理失败: {llm_e}"
@@ -524,12 +857,12 @@ class HeartFChatting:
anchor_message = MessageRecv(placeholder_msg_dict)
anchor_message.update_chat_stream(self.chat_stream)
logger.info(
- f"{self._get_log_prefix()} Created placeholder anchor message: ID={anchor_message.message_info.message_id}"
+ f"{self.log_prefix} Created placeholder anchor message: ID={anchor_message.message_info.message_id}"
)
return anchor_message
except Exception as e:
- logger.error(f"{self._get_log_prefix()} Error getting/creating anchor message: {e}")
+ logger.error(f"{self.log_prefix} Error getting/creating anchor message: {e}")
logger.error(traceback.format_exc())
return None
@@ -542,89 +875,135 @@ class HeartFChatting:
send_emoji: str, # Emoji query decided by planner or tools
):
"""
- 发送器 (Sender): 使用本类的方法发送生成的回复。
+ 发送器 (Sender): 使用 HeartFCSender 实例发送生成的回复。
处理相关的操作,如发送表情和更新关系。
"""
- log_prefix = self._get_log_prefix()
+ logger.info(f"{self.log_prefix}开始发送回复 (使用 HeartFCSender)")
first_bot_msg: Optional[MessageSending] = None
- # 尝试发送回复消息
- first_bot_msg = await self._send_response_messages(anchor_message, response_set, thinking_id)
- if first_bot_msg:
- # --- 处理关联表情(如果指定) --- #
- if send_emoji:
- logger.info(f"{log_prefix}[Sender-{thinking_id}] 正在发送关联表情: '{send_emoji}'")
- # 优先使用first_bot_msg作为锚点,否则回退到原始锚点
- emoji_anchor = first_bot_msg if first_bot_msg else anchor_message
- await self._handle_emoji(emoji_anchor, response_set, send_emoji)
+ try:
+ # _send_response_messages 现在将使用 self.sender 内部处理注册和发送
+ # 它需要负责创建 MessageThinking 和 MessageSending 对象
+ # 并调用 self.sender.register_thinking 和 self.sender.type_and_send_message
+ first_bot_msg = await self._send_response_messages(
+ anchor_message=anchor_message, response_set=response_set, thinking_id=thinking_id
+ )
- else:
- # logger.warning(f"{log_prefix}[Sender-{thinking_id}] 发送回复失败(_send_response_messages返回None)。思考消息{thinking_id}可能已被移除。")
- # 无需清理,因为_send_response_messages返回None意味着已处理/已删除
- raise RuntimeError("发送回复失败,_send_response_messages返回None")
+ if first_bot_msg:
+ # --- 处理关联表情(如果指定) --- #
+ if send_emoji:
+ logger.info(f"{self.log_prefix}正在发送关联表情: '{send_emoji}'")
+ # 优先使用 first_bot_msg 作为锚点,否则回退到原始锚点
+ emoji_anchor = first_bot_msg
+ await self._handle_emoji(emoji_anchor, response_set, send_emoji)
+ else:
+ # 如果 _send_response_messages 返回 None,表示在发送前就失败或没有消息可发送
+ logger.warning(
+ f"{self.log_prefix}[Sender-{thinking_id}] 未能发送任何回复消息 (_send_response_messages 返回 None)。"
+ )
+ # 这里可能不需要抛出异常,取决于 _send_response_messages 的具体实现
+
+ except Exception as e:
+ # 异常现在由 type_and_send_message 内部处理日志,这里只记录发送流程失败
+ logger.error(f"{self.log_prefix}[Sender-{thinking_id}] 发送回复过程中遇到错误: {e}")
+ # 思考状态应已在 type_and_send_message 的 finally 块中清理
+ # 可以选择重新抛出或根据业务逻辑处理
+ # raise RuntimeError(f"发送回复失败: {e}") from e
async def shutdown(self):
"""优雅关闭HeartFChatting实例,取消活动循环任务"""
- log_prefix = self._get_log_prefix()
- logger.info(f"{log_prefix} 正在关闭HeartFChatting...")
+ logger.info(f"{self.log_prefix} 正在关闭HeartFChatting...")
+ self._shutting_down = True # <-- 在开始关闭时设置标志位
# 取消循环任务
if self._loop_task and not self._loop_task.done():
- logger.info(f"{log_prefix} 正在取消HeartFChatting循环任务")
+ logger.info(f"{self.log_prefix} 正在取消HeartFChatting循环任务")
self._loop_task.cancel()
try:
await asyncio.wait_for(self._loop_task, timeout=1.0)
- logger.info(f"{log_prefix} HeartFChatting循环任务已取消")
+ logger.info(f"{self.log_prefix} HeartFChatting循环任务已取消")
except (asyncio.CancelledError, asyncio.TimeoutError):
pass
except Exception as e:
- logger.error(f"{log_prefix} 取消循环任务出错: {e}")
+ logger.error(f"{self.log_prefix} 取消循环任务出错: {e}")
else:
- logger.info(f"{log_prefix} 没有活动的HeartFChatting循环任务")
+ logger.info(f"{self.log_prefix} 没有活动的HeartFChatting循环任务")
# 清理状态
self._loop_active = False
self._loop_task = None
if self._processing_lock.locked():
self._processing_lock.release()
- logger.warning(f"{log_prefix} 已释放处理锁")
+ logger.warning(f"{self.log_prefix} 已释放处理锁")
- logger.info(f"{log_prefix} HeartFChatting关闭完成")
+ logger.info(f"{self.log_prefix} HeartFChatting关闭完成")
+
+ async def _build_replan_prompt(self, action: str, reasoning: str) -> str:
+ """构建 Replanner LLM 的提示词"""
+ prompt = (await global_prompt_manager.get_prompt_async("replan_prompt")).format(
+ action=action,
+ reasoning=reasoning,
+ )
+
+ # 在记录循环日志前检查关闭标志
+ if not self._shutting_down:
+ self._current_cycle.complete_cycle()
+ self._cycle_history.append(self._current_cycle)
+
+ # 记录循环信息和计时器结果
+ timer_strings = []
+ for name, elapsed in self._current_cycle.timers.items():
+ formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒"
+ timer_strings.append(f"{name}: {formatted_time}")
+
+ logger.debug(
+ f"{self.log_prefix} 第 #{self._current_cycle.cycle_id}次思考完成,"
+ f"耗时: {self._current_cycle.end_time - self._current_cycle.start_time:.2f}秒, "
+ f"动作: {self._current_cycle.action_type}"
+ + (f"\n计时器详情: {'; '.join(timer_strings)}" if timer_strings else "")
+ )
+
+ return prompt
async def _build_planner_prompt(
- self, observed_messages_str: str, current_mind: Optional[str], structured_info: Dict[str, Any]
+ self,
+ observed_messages_str: str,
+ current_mind: Optional[str],
+ structured_info: Dict[str, Any],
+ replan_prompt: str,
) -> str:
"""构建 Planner LLM 的提示词"""
-
+
# 准备结构化信息块
structured_info_block = ""
if structured_info:
structured_info_block = f"以下是一些额外的信息:\n{structured_info}\n"
-
+
# 准备聊天内容块
chat_content_block = ""
if observed_messages_str:
- chat_content_block = "观察到的最新聊天内容如下 (最近的消息在最后):\n---\n"
+ chat_content_block = "观察到的最新聊天内容如下:\n---\n"
chat_content_block += observed_messages_str
chat_content_block += "\n---"
else:
chat_content_block = "当前没有观察到新的聊天内容。\n"
-
+
# 准备当前思维块
current_mind_block = ""
if current_mind:
- current_mind_block = f"\n---\n{current_mind}\n---\n\n"
+ current_mind_block = f"{current_mind}"
else:
- current_mind_block = " [没有特别的想法] \n\n"
-
+ current_mind_block = "[没有特别的想法]"
+
# 获取提示词模板并填充数据
prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format(
bot_name=global_config.BOT_NICKNAME,
structured_info_block=structured_info_block,
chat_content_block=chat_content_block,
current_mind_block=current_mind_block,
+ replan=replan_prompt,
)
-
+
return prompt
# --- 回复器 (Replier) 的定义 --- #
@@ -637,7 +1016,6 @@ class HeartFChatting:
"""
回复器 (Replier): 核心逻辑用于生成回复。
"""
- log_prefix = self._get_log_prefix()
response_set: Optional[List[str]] = None
try:
response_set = await self.gpt_instance.generate_response(
@@ -649,13 +1027,13 @@ class HeartFChatting:
)
if not response_set:
- logger.warning(f"{log_prefix}[Replier-{thinking_id}] LLM生成了一个空回复集。")
+ logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM生成了一个空回复集。")
return None
return response_set
except Exception as e:
- logger.error(f"{log_prefix}[Replier-{thinking_id}] Unexpected error in replier_work: {e}")
+ logger.error(f"{self.log_prefix}[Replier-{thinking_id}] Unexpected error in replier_work: {e}")
logger.error(traceback.format_exc())
return None
@@ -663,7 +1041,7 @@ class HeartFChatting:
async def _create_thinking_message(self, anchor_message: Optional[MessageRecv]) -> Optional[str]:
"""创建思考消息 (尝试锚定到 anchor_message)"""
if not anchor_message or not anchor_message.chat_stream:
- logger.error(f"{self._get_log_prefix()} 无法创建思考消息,缺少有效的锚点消息或聊天流。")
+ logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。")
return None
chat = anchor_message.chat_stream
@@ -684,84 +1062,102 @@ class HeartFChatting:
thinking_start_time=thinking_time_point,
)
# Access MessageManager directly
- await message_manager.add_message(thinking_message)
+ await self.heart_fc_sender.register_thinking(thinking_message)
return thinking_id
async def _send_response_messages(
self, anchor_message: Optional[MessageRecv], response_set: List[str], thinking_id: str
) -> Optional[MessageSending]:
- """发送回复消息 (尝试锚定到 anchor_message)"""
+ """发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender"""
if not anchor_message or not anchor_message.chat_stream:
- logger.error(f"{self._get_log_prefix()} 无法发送回复,缺少有效的锚点消息或聊天流。")
+ logger.error(f"{self.log_prefix} 无法发送回复,缺少有效的锚点消息或聊天流。")
return None
chat = anchor_message.chat_stream
- container = await message_manager.get_container(chat.stream_id)
- thinking_message = None
+ chat_id = chat.stream_id
+ stream_name = chat_manager.get_stream_name(chat_id) or chat_id # 获取流名称用于日志
- # 移除思考消息
- for msg in container.messages[:]: # Iterate over a copy
- if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id:
- thinking_message = msg
- container.messages.remove(msg) # Remove the message directly here
- logger.debug(f"{self._get_log_prefix()} Removed thinking message {thinking_id} via iteration.")
- break
+ # 检查思考过程是否仍在进行,并获取开始时间
+ thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(chat_id, thinking_id)
- if not thinking_message:
- stream_name = chat_manager.get_stream_name(chat.stream_id) or chat.stream_id # 获取流名称
- logger.warning(f"[{stream_name}] {thinking_id},思考太久了,超时被移除")
+ if thinking_start_time is None:
+ logger.warning(f"[{stream_name}] {thinking_id} 思考过程未找到或已结束,无法发送回复。")
return None
- thinking_start_time = thinking_message.thinking_start_time
- message_set = MessageSet(chat, thinking_id)
+ # 记录锚点消息ID和回复文本(在发送前记录)
+ self._current_cycle.set_response_info(
+ response_text=response_set, anchor_message_id=anchor_message.message_info.message_id
+ )
+
mark_head = False
- first_bot_msg = None
+ first_bot_msg: Optional[MessageSending] = None
+ reply_message_ids = [] # 记录实际发送的消息ID
bot_user_info = UserInfo(
user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME,
platform=anchor_message.message_info.platform,
)
- for msg_text in response_set:
+
+ for i, msg_text in enumerate(response_set):
+ # 为每个消息片段生成唯一ID
+ part_message_id = f"{thinking_id}_{i}"
message_segment = Seg(type="text", data=msg_text)
bot_message = MessageSending(
- message_id=thinking_id, # 使用 thinking_id 作为批次标识
+ message_id=part_message_id, # 使用片段的唯一ID
chat_stream=chat,
bot_user_info=bot_user_info,
- sender_info=anchor_message.message_info.user_info, # 发送给锚点消息的用户
+ sender_info=anchor_message.message_info.user_info,
message_segment=message_segment,
- reply=anchor_message, # 回复锚点消息
+ reply=anchor_message, # 回复原始锚点
is_head=not mark_head,
is_emoji=False,
- thinking_start_time=thinking_start_time,
+ thinking_start_time=thinking_start_time, # 传递原始思考开始时间
)
- if not mark_head:
- mark_head = True
- first_bot_msg = bot_message
- message_set.add_message(bot_message)
+ try:
+ if not mark_head:
+ mark_head = True
+ first_bot_msg = bot_message # 保存第一个成功发送的消息对象
+ await self.heart_fc_sender.type_and_send_message(bot_message, type=False)
+ else:
+ await self.heart_fc_sender.type_and_send_message(bot_message, type=True)
- # Access MessageManager directly
- await message_manager.add_message(message_set)
- return first_bot_msg
+ reply_message_ids.append(part_message_id) # 记录我们生成的ID
+
+ except Exception as e:
+ logger.error(
+ f"{self.log_prefix}[Sender-{thinking_id}] 发送回复片段 {i} ({part_message_id}) 时失败: {e}"
+ )
+ # 这里可以选择是继续发送下一个片段还是中止
+
+ # 在尝试发送完所有片段后,完成原始的 thinking_id 状态
+ try:
+ await self.heart_fc_sender.complete_thinking(chat_id, thinking_id)
+ except Exception as e:
+ logger.error(f"{self.log_prefix}[Sender-{thinking_id}] 完成思考状态 {thinking_id} 时出错: {e}")
+
+ self._current_cycle.set_response_info(
+ response_text=response_set, # 保留原始文本
+ anchor_message_id=anchor_message.message_info.message_id, # 保留锚点ID
+ reply_message_ids=reply_message_ids, # 添加实际发送的ID列表
+ )
+
+ return first_bot_msg # 返回第一个成功发送的消息对象
async def _handle_emoji(self, anchor_message: Optional[MessageRecv], response_set: List[str], send_emoji: str = ""):
- """处理表情包 (尝试锚定到 anchor_message)"""
-
+ """处理表情包 (尝试锚定到 anchor_message),使用 HeartFCSender"""
if not anchor_message or not anchor_message.chat_stream:
- logger.error(f"{self._get_log_prefix()} 无法处理表情包,缺少有效的锚点消息或聊天流。")
+ logger.error(f"{self.log_prefix} 无法处理表情包,缺少有效的锚点消息或聊天流。")
return
chat = anchor_message.chat_stream
- if send_emoji:
- emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji)
- else:
- emoji_text_source = "".join(response_set) if response_set else ""
- emoji_raw = await emoji_manager.get_emoji_for_text(emoji_text_source)
+ emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji)
if emoji_raw:
- emoji_path, _description = emoji_raw
+ emoji_path, description = emoji_raw
+
emoji_cq = image_path_to_base64(emoji_path)
- thinking_time_point = round(time.time(), 2)
+ thinking_time_point = round(time.time(), 2) # 用于唯一ID
message_segment = Seg(type="emoji", data=emoji_cq)
bot_user_info = UserInfo(
user_id=global_config.BOT_QQ,
@@ -769,14 +1165,38 @@ class HeartFChatting:
platform=anchor_message.message_info.platform,
)
bot_message = MessageSending(
- message_id="me" + str(thinking_time_point), # 使用不同的 ID 前缀?
+ message_id="me" + str(thinking_time_point), # 表情消息的唯一ID
chat_stream=chat,
bot_user_info=bot_user_info,
sender_info=anchor_message.message_info.user_info,
message_segment=message_segment,
- reply=anchor_message, # 回复锚点消息
- is_head=False,
+ reply=anchor_message, # 回复原始锚点
+ is_head=False, # 表情通常不是头部消息
is_emoji=True,
+ # 不需要 thinking_start_time
)
- # Access MessageManager directly
- await message_manager.add_message(bot_message)
+
+ try:
+ await self.heart_fc_sender.send_and_store(bot_message)
+ except Exception as e:
+ logger.error(f"{self.log_prefix} 发送表情包 {bot_message.message_info.message_id} 时失败: {e}")
+
+ def get_cycle_history(self, last_n: Optional[int] = None) -> List[Dict[str, Any]]:
+ """获取循环历史记录
+
+ 参数:
+ last_n: 获取最近n个循环的信息,如果为None则获取所有历史记录
+
+ 返回:
+ List[Dict[str, Any]]: 循环历史记录列表
+ """
+ history = list(self._cycle_history)
+ if last_n is not None:
+ history = history[-last_n:]
+ return [cycle.to_dict() for cycle in history]
+
+ def get_last_cycle_info(self) -> Optional[Dict[str, Any]]:
+ """获取最近一个循环的信息"""
+ if self._cycle_history:
+ return self._cycle_history[-1].to_dict()
+ return None
diff --git a/src/plugins/heartFC_chat/heartFC_chatting_logic.md b/src/plugins/heartFC_chat/heartFC_chatting_logic.md
new file mode 100644
index 000000000..6d51c978b
--- /dev/null
+++ b/src/plugins/heartFC_chat/heartFC_chatting_logic.md
@@ -0,0 +1,92 @@
+# HeartFChatting 逻辑详解
+
+`HeartFChatting` 类是心流系统(Heart Flow System)中实现**专注聊天**(`ChatState.FOCUSED`)功能的核心。顾名思义,其职责乃是在特定聊天流(`stream_id`)中,模拟更为连贯深入之对话。此非凭空臆造,而是依赖一个持续不断的 **思考(Think)-规划(Plan)-执行(Execute)** 循环。当其所系的 `SubHeartflow` 进入 `FOCUSED` 状态时,便会创建并启动 `HeartFChatting` 实例;若状态转为他途(譬如 `CHAT` 或 `ABSENT`),则会将其关闭。
+
+## 1. 初始化简述 (`__init__`, `_initialize`)
+
+创生之初,`HeartFChatting` 需注入若干关键之物:`chat_id`(亦即 `stream_id`)、关联的 `SubMind` 实例,以及 `Observation` 实例(用以观察环境)。
+
+其内部核心组件包括:
+
+- `ActionManager`: 管理当前循环可选之策(如:不应、言语、表情)。
+- `HeartFCGenerator` (`self.gpt_instance`): 专司生成回复文本之职。
+- `ToolUser` (`self.tool_user`): 虽主要用于获取工具定义,然亦备 `SubMind` 调用之需(实际执行由 `SubMind` 操持)。
+- `HeartFCSender` (`self.heart_fc_sender`): 负责消息发送诸般事宜,含"正在思考"之态。
+- `LLMRequest` (`self.planner_llm`): 配置用于执行"规划"任务的大语言模型。
+
+*初始化过程采取懒加载策略,仅在首次需要访问 `ChatStream` 时(通常在 `start` 方法中)进行。*
+
+## 2. 生命周期 (`start`, `shutdown`)
+
+- **启动 (`start`)**: 外部调用此法,以启 `HeartFChatting` 之流程。内部会安全地启动主循环任务。
+- **关闭 (`shutdown`)**: 外部调用此法,以止其运行。会取消主循环任务,清理状态,并释放锁。
+
+## 3. 核心循环 (`_hfc_loop`) 与 循环记录 (`CycleInfo`)
+
+`_hfc_loop` 乃 `HeartFChatting` 之脉搏,以异步方式不舍昼夜运行(直至 `shutdown` 被调用)。其核心在于周而复始地执行 **思考-规划-执行** 之周期。
+
+每一轮循环,皆会创建一个 `CycleInfo` 对象。此对象犹如史官,详细记载该次循环之点滴:
+
+- **身份标识**: 循环 ID (`cycle_id`)。
+- **时间轨迹**: 起止时刻 (`start_time`, `end_time`)。
+- **行动细节**: 是否执行动作 (`action_taken`)、动作类型 (`action_type`)、决策理由 (`reasoning`)。
+- **耗时考量**: 各阶段计时 (`timers`)。
+- **关联信息**: 思考消息 ID (`thinking_id`)、是否重新规划 (`replanned`)、详尽响应信息 (`response_info`,含生成文本、表情、锚点、实际发送ID、`SubMind`思考等)。
+
+这些 `CycleInfo` 被存入一个队列 (`_cycle_history`),近者得观。此记录不仅便于调试,更关键的是,它会作为**上下文信息**传递给下一次循环的"思考"阶段,使得 `SubMind` 能鉴往知来,做出更连贯的决策。
+
+*循环间会根据执行情况智能引入延迟,避免空耗资源。*
+
+## 4. 思考-规划-执行周期 (`_think_plan_execute_loop`)
+
+此乃 `HeartFChatting` 最核心的逻辑单元,每一循环皆按序执行以下三步:
+
+### 4.1. 思考 (`_get_submind_thinking`)
+
+* **第一步:观察环境**: 调用 `Observation` 的 `observe()` 方法,感知聊天室是否有新动态(如新消息)。
+* **第二步:触发子思维**: 调用关联 `SubMind` 的 `do_thinking_before_reply()` 方法。
+ * **关键点**: 会将**上一个循环**的 `CycleInfo` 传入,让 `SubMind` 了解上次行动的决策、理由及是否重新规划,从而实现"承前启后"的思考。
+ * `SubMind` 在此阶段不仅进行思考,还可能**调用其配置的工具**来收集信息。
+* **第三步:获取成果**: `SubMind` 返回两部分重要信息:
+ 1. 当前的内心想法 (`current_mind`)。
+ 2. 通过工具调用收集到的结构化信息 (`structured_info`)。
+
+### 4.2. 规划 (`_planner`)
+
+* **输入**: 接收来自"思考"阶段的 `current_mind` 和 `structured_info`,以及"观察"到的最新消息。
+* **目标**: 基于当前想法、已知信息、聊天记录、机器人个性以及可用动作,决定**接下来要做什么**。
+* **决策方式**:
+ 1. 构建一个精心设计的提示词 (`_build_planner_prompt`)。
+ 2. 获取 `ActionManager` 中定义的当前可用动作(如 `no_reply`, `text_reply`, `emoji_reply`)作为"工具"选项。
+ 3. 调用大语言模型 (`self.planner_llm`),**强制**其选择一个动作"工具"并提供理由。可选动作包括:
+ * `no_reply`: 不回复(例如,自己刚说过话或对方未回应)。
+ * `text_reply`: 发送文本回复。
+ * `emoji_reply`: 仅发送表情。
+ * 文本回复亦可附带表情(通过 `emoji_query` 参数指定)。
+* **动态调整(重新规划)**:
+ * 在做出初步决策后,会检查自规划开始后是否有新消息 (`_check_new_messages`)。
+ * 若有新消息,则有一定概率触发**重新规划**。此时会再次调用规划器,但提示词会包含之前决策的信息,要求 LLM 重新考虑。
+* **输出**: 返回一个包含最终决策的字典,主要包括:
+ * `action`: 选定的动作类型。
+ * `reasoning`: 做出此决策的理由。
+ * `emoji_query`: (可选) 如果需要发送表情,指定表情的主题。
+
+### 4.3. 执行 (`_handle_action`)
+
+* **输入**: 接收"规划"阶段输出的 `action`、`reasoning` 和 `emoji_query`。
+* **行动**: 根据 `action` 的类型,分派到不同的处理函数:
+ * **文本回复 (`_handle_text_reply`)**:
+ 1. 获取锚点消息(当前实现为系统触发的占位符)。
+ 2. 调用 `HeartFCSender` 的 `register_thinking` 标记开始思考。
+ 3. 调用 `HeartFCGenerator` (`_replier_work`) 生成回复文本。**注意**: 回复器逻辑 (`_replier_work`) 本身并非独立复杂组件,主要是调用 `HeartFCGenerator` 完成文本生成。
+ 4. 调用 `HeartFCSender` (`_sender`) 发送生成的文本和可能的表情。**注意**: 发送逻辑 (`_sender`, `_send_response_messages`, `_handle_emoji`) 同样委托给 `HeartFCSender` 实例处理,包含模拟打字、实际发送、存储消息等细节。
+ * **仅表情回复 (`_handle_emoji_reply`)**:
+ 1. 获取锚点消息。
+ 2. 调用 `HeartFCSender` 发送表情。
+ * **不回复 (`_handle_no_reply`)**:
+ 1. 记录理由。
+ 2. 进入等待状态 (`_wait_for_new_message`),直到检测到新消息或超时(目前300秒),期间会监听关闭信号。
+
+## 总结
+
+`HeartFChatting` 通过 **观察 -> 思考(含工具)-> 规划 -> 执行** 的闭环,并利用 `CycleInfo` 进行上下文传递,实现了更加智能和连贯的专注聊天行为。其核心在于利用 `SubMind` 进行深度思考和信息收集,再通过 LLM 规划器进行决策,最后由 `HeartFCSender` 可靠地执行消息发送任务。
diff --git a/src/plugins/heartFC_chat/heartFC_generator.py b/src/plugins/heartFC_chat/heartFC_generator.py
index 464e94e91..43b4d87da 100644
--- a/src/plugins/heartFC_chat/heartFC_generator.py
+++ b/src/plugins/heartFC_chat/heartFC_generator.py
@@ -8,7 +8,7 @@ from .heartflow_prompt_builder import prompt_builder
from ..chat.utils import process_llm_response
from src.common.logger import get_module_logger, LogConfig, LLM_STYLE_CONFIG
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
-from ..utils.timer_calculater import Timer
+from ..utils.timer_calculator import Timer
from src.plugins.moods.moods import MoodManager
@@ -49,17 +49,13 @@ class HeartFCGenerator:
arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()
- with Timer() as t_generate_response:
- current_model = self.model_normal
- current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier # 激活度越高,温度越高
- model_response = await self._generate_response_with_model(
- structured_info, current_mind_info, reason, message, current_model, thinking_id
- )
+ current_model = self.model_normal
+ current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier # 激活度越高,温度越高
+ model_response = await self._generate_response_with_model(
+ structured_info, current_mind_info, reason, message, current_model, thinking_id
+ )
if model_response:
- logger.info(
- f"{global_config.BOT_NICKNAME}的回复是:{model_response},生成回复时间: {t_generate_response.human_readable}"
- )
model_processed_response = await self._process_response(model_response)
return model_processed_response
@@ -78,7 +74,7 @@ class HeartFCGenerator:
) -> str:
info_catcher = info_catcher_manager.get_info_catcher(thinking_id)
- with Timer() as t_build_prompt:
+ with Timer() as _build_prompt:
prompt = await prompt_builder.build_prompt(
build_mode="focus",
reason=reason,
diff --git a/src/plugins/heartFC_chat/heartFC_readme.md b/src/plugins/heartFC_chat/heartFC_readme.md
new file mode 100644
index 000000000..07bc4c63c
--- /dev/null
+++ b/src/plugins/heartFC_chat/heartFC_readme.md
@@ -0,0 +1,159 @@
+# HeartFC_chat 工作原理文档
+
+HeartFC_chat 是一个基于心流理论的聊天系统,通过模拟人类的思维过程和情感变化来实现自然的对话交互。系统采用Plan-Replier-Sender循环机制,实现了智能化的对话决策和生成。
+
+## 核心工作流程
+
+### 1. 消息处理与存储 (HeartFCProcessor)
+[代码位置: src/plugins/heartFC_chat/heartflow_processor.py]
+
+消息处理器负责接收和预处理消息,主要完成以下工作:
+```mermaid
+graph TD
+ A[接收原始消息] --> B[解析为MessageRecv对象]
+ B --> C[消息缓冲处理]
+ C --> D[过滤检查]
+ D --> E[存储到数据库]
+```
+
+核心实现:
+- 消息处理入口:`process_message()` [行号: 38-215]
+ - 消息解析和缓冲:`message_buffer.start_caching_messages()` [行号: 63]
+ - 过滤检查:`_check_ban_words()`, `_check_ban_regex()` [行号: 196-215]
+ - 消息存储:`storage.store_message()` [行号: 108]
+
+### 2. 对话管理循环 (HeartFChatting)
+[代码位置: src/plugins/heartFC_chat/heartFC_chat.py]
+
+HeartFChatting是系统的核心组件,实现了完整的对话管理循环:
+
+```mermaid
+graph TD
+ A[Plan阶段] -->|决策是否回复| B[Replier阶段]
+ B -->|生成回复内容| C[Sender阶段]
+ C -->|发送消息| D[等待新消息]
+ D --> A
+```
+
+#### Plan阶段 [行号: 282-386]
+- 主要函数:`_planner()`
+- 功能实现:
+ * 获取观察信息:`observation.observe()` [行号: 297]
+ * 思维处理:`sub_mind.do_thinking_before_reply()` [行号: 301]
+ * LLM决策:使用`PLANNER_TOOL_DEFINITION`进行动作规划 [行号: 13-42]
+
+#### Replier阶段 [行号: 388-416]
+- 主要函数:`_replier_work()`
+- 调用生成器:`gpt_instance.generate_response()` [行号: 394]
+- 处理生成结果和错误情况
+
+#### Sender阶段 [行号: 418-450]
+- 主要函数:`_sender()`
+- 发送实现:
+ * 创建消息:`_create_thinking_message()` [行号: 452-477]
+ * 发送回复:`_send_response_messages()` [行号: 479-525]
+ * 处理表情:`_handle_emoji()` [行号: 527-567]
+
+### 3. 回复生成机制 (HeartFCGenerator)
+[代码位置: src/plugins/heartFC_chat/heartFC_generator.py]
+
+回复生成器负责产生高质量的回复内容:
+
+```mermaid
+graph TD
+ A[获取上下文信息] --> B[构建提示词]
+ B --> C[调用LLM生成]
+ C --> D[后处理优化]
+ D --> E[返回回复集]
+```
+
+核心实现:
+- 生成入口:`generate_response()` [行号: 39-67]
+ * 情感调节:`arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()` [行号: 47]
+ * 模型生成:`_generate_response_with_model()` [行号: 69-95]
+ * 响应处理:`_process_response()` [行号: 97-106]
+
+### 4. 提示词构建系统 (HeartFlowPromptBuilder)
+[代码位置: src/plugins/heartFC_chat/heartflow_prompt_builder.py]
+
+提示词构建器支持两种工作模式,HeartFC_chat专门使用Focus模式,而Normal模式是为normal_chat设计的:
+
+#### 专注模式 (Focus Mode) - HeartFC_chat专用
+- 实现函数:`_build_prompt_focus()` [行号: 116-141]
+- 特点:
+ * 专注于当前对话状态和思维
+ * 更强的目标导向性
+ * 用于HeartFC_chat的Plan-Replier-Sender循环
+ * 简化的上下文处理,专注于决策
+
+#### 普通模式 (Normal Mode) - Normal_chat专用
+- 实现函数:`_build_prompt_normal()` [行号: 143-215]
+- 特点:
+ * 用于normal_chat的常规对话
+ * 完整的个性化处理
+ * 关系系统集成
+ * 知识库检索:`get_prompt_info()` [行号: 217-591]
+
+HeartFC_chat的Focus模式工作流程:
+```mermaid
+graph TD
+ A[获取结构化信息] --> B[获取当前思维状态]
+ B --> C[构建专注模式提示词]
+ C --> D[用于Plan阶段决策]
+ D --> E[用于Replier阶段生成]
+```
+
+## 智能特性
+
+### 1. 对话决策机制
+- LLM决策工具定义:`PLANNER_TOOL_DEFINITION` [heartFC_chat.py 行号: 13-42]
+- 决策执行:`_planner()` [heartFC_chat.py 行号: 282-386]
+- 考虑因素:
+ * 上下文相关性
+ * 情感状态
+ * 兴趣程度
+ * 对话时机
+
+### 2. 状态管理
+[代码位置: src/plugins/heartFC_chat/heartFC_chat.py]
+- 状态机实现:`HeartFChatting`类 [行号: 44-567]
+- 核心功能:
+ * 初始化:`_initialize()` [行号: 89-112]
+ * 循环控制:`_run_pf_loop()` [行号: 192-281]
+ * 状态转换:`_handle_loop_completion()` [行号: 166-190]
+
+### 3. 回复生成策略
+[代码位置: src/plugins/heartFC_chat/heartFC_generator.py]
+- 温度调节:`current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier` [行号: 48]
+- 生成控制:`_generate_response_with_model()` [行号: 69-95]
+- 响应处理:`_process_response()` [行号: 97-106]
+
+## 系统配置
+
+### 关键参数
+- LLM配置:`model_normal` [heartFC_generator.py 行号: 32-37]
+- 过滤规则:`_check_ban_words()`, `_check_ban_regex()` [heartflow_processor.py 行号: 196-215]
+- 状态控制:`INITIAL_DURATION = 60.0` [heartFC_chat.py 行号: 11]
+
+### 优化建议
+1. 调整LLM参数:`temperature`和`max_tokens`
+2. 优化提示词模板:`init_prompt()` [heartflow_prompt_builder.py 行号: 8-115]
+3. 配置状态转换条件
+4. 维护过滤规则
+
+## 注意事项
+
+1. 系统稳定性
+- 异常处理:各主要函数都包含try-except块
+- 状态检查:`_processing_lock`确保并发安全
+- 循环控制:`_loop_active`和`_loop_task`管理
+
+2. 性能优化
+- 缓存使用:`message_buffer`系统
+- LLM调用优化:批量处理和复用
+- 异步处理:使用`asyncio`
+
+3. 质量控制
+- 日志记录:使用`get_module_logger()`
+- 错误追踪:详细的异常记录
+- 响应监控:完整的状态跟踪
diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py
new file mode 100644
index 000000000..d436c668a
--- /dev/null
+++ b/src/plugins/heartFC_chat/heartFC_sender.py
@@ -0,0 +1,153 @@
+# src/plugins/heartFC_chat/heartFC_sender.py
+import asyncio # 重新导入 asyncio
+from typing import Dict, Optional # 重新导入类型
+
+from src.common.logger import get_module_logger
+from ..message.api import global_api
+from ..chat.message import MessageSending, MessageThinking # 只保留 MessageSending 和 MessageThinking
+from ..storage.storage import MessageStorage
+from ..chat.utils import truncate_message
+from src.common.logger import LogConfig, SENDER_STYLE_CONFIG
+from src.plugins.chat.utils import calculate_typing_time
+
+# 定义日志配置
+sender_config = LogConfig(
+ # 使用消息发送专用样式
+ console_format=SENDER_STYLE_CONFIG["console_format"],
+ file_format=SENDER_STYLE_CONFIG["file_format"],
+)
+
+logger = get_module_logger("msg_sender", config=sender_config)
+
+
+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 send_message(self, message: MessageSending) -> None:
+ """合并后的消息发送函数,包含WS发送和日志记录"""
+ message_preview = truncate_message(message.processed_plain_text)
+
+ try:
+ # 直接调用API发送消息
+ await global_api.send_message(message)
+ logger.success(f"发送消息 '{message_preview}' 成功")
+
+ except Exception as e:
+ logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}")
+ if not message.message_info.platform:
+ raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e
+ raise e # 重新抛出其他异常
+
+ 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.")
+
+ def is_thinking(self, chat_id: str, message_id: str) -> bool:
+ """检查指定的消息 ID 是否当前正处于思考状态。"""
+ return chat_id in self.thinking_messages and message_id in self.thinking_messages[chat_id]
+
+ 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 type_and_send_message(self, message: MessageSending, type=False):
+ """
+ 立即处理、发送并存储单个 MessageSending 消息。
+ 调用此方法前,应先调用 register_thinking 注册对应的思考消息。
+ 此方法执行后会调用 complete_thinking 清理思考状态。
+ """
+ if not message.chat_stream:
+ logger.error("消息缺少 chat_stream,无法发送")
+ return
+ if not message.message_info or not message.message_info.message_id:
+ logger.error("消息缺少 message_info 或 message_id,无法发送")
+ return
+
+ chat_id = message.chat_stream.stream_id
+ message_id = message.message_info.message_id
+
+ try:
+ _ = message.update_thinking_time()
+
+ # --- 条件应用 set_reply 逻辑 ---
+ if message.apply_set_reply_logic and message.is_head and not message.is_private_message():
+ logger.debug(f"[{chat_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}...")
+ message.set_reply()
+ # --- 结束条件 set_reply ---
+
+ await message.process()
+
+ if type:
+ typing_time = calculate_typing_time(
+ input_string=message.processed_plain_text,
+ thinking_start_time=message.thinking_start_time,
+ is_emoji=message.is_emoji,
+ )
+ await asyncio.sleep(typing_time)
+
+ await self.send_message(message)
+ await self.storage.store_message(message, message.chat_stream)
+
+ except Exception as e:
+ logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}")
+ raise e
+ finally:
+ await self.complete_thinking(chat_id, message_id)
+
+ async def send_and_store(self, message: MessageSending):
+ """处理、发送并存储单个消息,不涉及思考状态管理。"""
+ if not message.chat_stream:
+ logger.error(f"[{message.message_info.platform or 'UnknownPlatform'}] 消息缺少 chat_stream,无法发送")
+ return
+ if not message.message_info or not message.message_info.message_id:
+ logger.error(
+ f"[{message.chat_stream.stream_id if message.chat_stream else 'UnknownStream'}] 消息缺少 message_info 或 message_id,无法发送"
+ )
+ return
+
+ chat_id = message.chat_stream.stream_id
+ message_id = message.message_info.message_id # 获取消息ID用于日志
+
+ try:
+ await message.process()
+
+ await asyncio.sleep(0.5)
+
+ await self.send_message(message) # 使用现有的发送方法
+ await self.storage.store_message(message, message.chat_stream) # 使用现有的存储方法
+
+ except Exception as e:
+ logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}")
+ # 重新抛出异常,让调用者知道失败了
+ raise e
diff --git a/src/plugins/heartFC_chat/heartflow_processor.py b/src/plugins/heartFC_chat/heartflow_processor.py
index f7c3a64fd..c907f98a8 100644
--- a/src/plugins/heartFC_chat/heartflow_processor.py
+++ b/src/plugins/heartFC_chat/heartflow_processor.py
@@ -5,13 +5,14 @@ from ...config.config import global_config
from ..chat.message import MessageRecv
from ..storage.storage import MessageStorage
from ..chat.utils import is_mentioned_bot_in_message
-from ..message import Seg
+from maim_message import Seg
from src.heart_flow.heartflow import heartflow
from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig
from ..chat.chat_stream import chat_manager
from ..chat.message_buffer import message_buffer
-from ..utils.timer_calculater import Timer
+from ..utils.timer_calculator import Timer
from src.plugins.person_info.relationship_manager import relationship_manager
+from typing import Optional, Tuple
# 定义日志配置
processor_config = LogConfig(
@@ -22,193 +23,202 @@ logger = get_module_logger("heartflow_processor", config=processor_config)
class HeartFCProcessor:
+ """心流处理器,负责处理接收到的消息并计算兴趣度"""
+
def __init__(self):
+ """初始化心流处理器,创建消息存储实例"""
self.storage = MessageStorage()
- async def process_message(self, message_data: str) -> None:
- """处理接收到的原始消息数据,完成消息解析、缓冲、过滤、存储、兴趣度计算与更新等核心流程。
-
- 此函数是消息处理的核心入口,负责接收原始字符串格式的消息数据,并将其转化为结构化的 `MessageRecv` 对象。
- 主要执行步骤包括:
- 1. 解析 `message_data` 为 `MessageRecv` 对象,提取用户信息、群组信息等。
- 2. 将消息加入 `message_buffer` 进行缓冲处理,以应对消息轰炸或者某些人一条消息分几次发等情况。
- 3. 获取或创建对应的 `chat_stream` 和 `subheartflow` 实例,用于管理会话状态和心流。
- 4. 对消息内容进行初步处理(如提取纯文本)。
- 5. 应用全局配置中的过滤词和正则表达式,过滤不符合规则的消息。
- 6. 查询消息缓冲结果,如果消息被缓冲器拦截(例如,判断为消息轰炸的一部分),则中止后续处理。
- 7. 对于通过缓冲的消息,将其存储到 `MessageStorage` 中。
-
- 8. 调用海马体(`HippocampusManager`)计算消息内容的记忆激活率。(这部分算法后续会进行优化)
- 9. 根据是否被提及(@)和记忆激活率,计算最终的兴趣度增量。(提及的额外兴趣增幅)
- 10. 使用计算出的增量更新 `InterestManager` 中对应会话的兴趣度。
- 11. 记录处理后的消息信息及当前的兴趣度到日志。
-
- 注意:此函数本身不负责生成和发送回复。回复的决策和生成逻辑被移至 `HeartFC_Chat` 类中的监控任务,
- 该任务会根据 `InterestManager` 中的兴趣度变化来决定何时触发回复。
+ async def _handle_error(self, error: Exception, context: str, message: Optional[MessageRecv] = None) -> None:
+ """统一的错误处理函数
Args:
- message_data: str: 从消息源接收到的原始消息字符串。
+ error: 捕获到的异常
+ context: 错误发生的上下文描述
+ message: 可选的消息对象,用于记录相关消息内容
+ """
+ logger.error(f"{context}: {error}")
+ logger.error(traceback.format_exc())
+ if message and hasattr(message, "raw_message"):
+ logger.error(f"相关消息原始内容: {message.raw_message}")
+
+ async def _process_relationship(self, message: MessageRecv) -> None:
+ """处理用户关系逻辑
+
+ Args:
+ message: 消息对象,包含用户信息
+ """
+ platform = message.message_info.platform
+ user_id = message.message_info.user_info.user_id
+ nickname = message.message_info.user_info.user_nickname
+ cardname = message.message_info.user_info.user_cardname or nickname
+
+ is_known = await relationship_manager.is_known_some_one(platform, user_id)
+
+ if not is_known:
+ logger.info(f"首次认识用户: {nickname}")
+ await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "")
+ elif not await relationship_manager.is_qved_name(platform, user_id):
+ logger.info(f"给用户({nickname},{cardname})取名: {nickname}")
+ await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "")
+
+ async def _calculate_interest(self, message: MessageRecv) -> Tuple[float, bool]:
+ """计算消息的兴趣度
+
+ Args:
+ message: 待处理的消息对象
+
+ Returns:
+ Tuple[float, bool]: (兴趣度, 是否被提及)
+ """
+ is_mentioned, _ = is_mentioned_bot_in_message(message)
+ interested_rate = 0.0
+
+ with Timer("记忆激活"):
+ interested_rate = await HippocampusManager.get_instance().get_activate_from_text(
+ message.processed_plain_text,
+ fast_retrieval=True,
+ )
+ logger.trace(f"记忆激活率: {interested_rate:.2f}")
+
+ if is_mentioned:
+ interest_increase_on_mention = 1
+ interested_rate += interest_increase_on_mention
+
+ return interested_rate, is_mentioned
+
+ def _get_message_type(self, message: MessageRecv) -> str:
+ """获取消息类型
+
+ Args:
+ message: 消息对象
+
+ Returns:
+ str: 消息类型
+ """
+ if message.message_segment.type != "seglist":
+ return message.message_segment.type
+
+ if (
+ isinstance(message.message_segment.data, list)
+ and all(isinstance(x, Seg) for x in message.message_segment.data)
+ and len(message.message_segment.data) == 1
+ ):
+ return message.message_segment.data[0].type
+
+ return "seglist"
+
+ async def process_message(self, message_data: str) -> None:
+ """处理接收到的原始消息数据
+
+ 主要流程:
+ 1. 消息解析与初始化
+ 2. 消息缓冲处理
+ 3. 过滤检查
+ 4. 兴趣度计算
+ 5. 关系处理
+
+ Args:
+ message_data: 原始消息字符串
"""
- timing_results = {} # 初始化 timing_results
message = None
try:
+ # 1. 消息解析与初始化
message = MessageRecv(message_data)
groupinfo = message.message_info.group_info
userinfo = message.message_info.user_info
messageinfo = message.message_info
- # 消息加入缓冲池
+ # 2. 消息缓冲与流程序化
await message_buffer.start_caching_messages(message)
- # 创建聊天流
chat = await chat_manager.get_or_create_stream(
platform=messageinfo.platform,
user_info=userinfo,
group_info=groupinfo,
)
- subheartflow = await heartflow.create_subheartflow(chat.stream_id)
-
+ subheartflow = await heartflow.get_or_create_subheartflow(chat.stream_id)
message.update_chat_stream(chat)
-
- await heartflow.create_subheartflow(chat.stream_id)
-
await message.process()
- logger.trace(f"消息处理成功: {message.processed_plain_text}")
- # 过滤词/正则表达式过滤
+ # 3. 过滤检查
if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex(
message.raw_message, chat, userinfo
):
return
- # 查询缓冲器结果
+ # 4. 缓冲检查
buffer_result = await message_buffer.query_buffer_result(message)
-
- # 处理缓冲器结果 (Bombing logic)
if not buffer_result:
- f_type = "seglist"
- if message.message_segment.type != "seglist":
- f_type = message.message_segment.type
- else:
- if (
- isinstance(message.message_segment.data, list)
- and all(isinstance(x, Seg) for x in message.message_segment.data)
- and len(message.message_segment.data) == 1
- ):
- f_type = message.message_segment.data[0].type
- if f_type == "text":
- logger.debug(f"触发缓冲,消息:{message.processed_plain_text}")
- elif f_type == "image":
- logger.debug("触发缓冲,表情包/图片等待中")
- elif f_type == "seglist":
- logger.debug("触发缓冲,消息列表等待中")
- return # 被缓冲器拦截,不生成回复
-
- # ---- 只有通过缓冲的消息才进行存储和后续处理 ----
-
- # 存储消息 (使用可能被缓冲器更新过的 message)
- try:
- await self.storage.store_message(message, chat)
- logger.trace(f"存储成功 (通过缓冲后): {message.processed_plain_text}")
- except Exception as e:
- logger.error(f"存储消息失败: {e}")
- logger.error(traceback.format_exc())
- # 存储失败可能仍需考虑是否继续,暂时返回
+ msg_type = self._get_message_type(message)
+ type_messages = {
+ "text": f"触发缓冲,消息:{message.processed_plain_text}",
+ "image": "触发缓冲,表情包/图片等待中",
+ "seglist": "触发缓冲,消息列表等待中",
+ }
+ logger.debug(type_messages.get(msg_type, "触发未知类型缓冲"))
return
- # 激活度计算 (使用可能被缓冲器更新过的 message.processed_plain_text)
- is_mentioned, _ = is_mentioned_bot_in_message(message)
- interested_rate = 0.0 # 默认值
- try:
- with Timer("记忆激活", timing_results):
- interested_rate = await HippocampusManager.get_instance().get_activate_from_text(
- message.processed_plain_text,
- fast_retrieval=True, # 使用更新后的文本
- )
- logger.trace(f"记忆激活率 (通过缓冲后): {interested_rate:.2f}")
- except Exception as e:
- logger.error(f"计算记忆激活率失败: {e}")
- logger.error(traceback.format_exc())
+ # 5. 消息存储
+ await self.storage.store_message(message, chat)
+ logger.trace(f"存储成功: {message.processed_plain_text}")
- # --- 修改:兴趣度更新逻辑 --- #
- if is_mentioned:
- interest_increase_on_mention = 1
- mentioned_boost = interest_increase_on_mention # 从配置获取提及增加值
- interested_rate += mentioned_boost
+ # 6. 兴趣度计算与更新
+ interested_rate, is_mentioned = await self._calculate_interest(message)
+ await subheartflow.interest_chatting.increase_interest(value=interested_rate)
+ subheartflow.interest_chatting.add_interest_dict(message, interested_rate, is_mentioned)
- # 更新兴趣度 (调用 SubHeartflow 的方法)
- current_time = time.time()
- await subheartflow.interest_chatting.increase_interest(current_time, value=interested_rate)
-
- # 添加到 SubHeartflow 的 interest_dict,给normal_chat处理
- await subheartflow.add_interest_dict_entry(message, interested_rate, is_mentioned)
-
- # 打印消息接收和处理信息
+ # 7. 日志记录
mes_name = chat.group_info.group_name if chat.group_info else "私聊"
- current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time))
+ current_time = time.strftime("%H点%M分%S秒", time.localtime(message.message_info.time))
logger.info(
f"[{current_time}][{mes_name}]"
- f"{message.message_info.user_info.user_nickname}:"
+ f"{userinfo.user_nickname}:"
f"{message.processed_plain_text}"
f"[兴趣度: {interested_rate:.2f}]"
)
- try:
- is_known = await relationship_manager.is_known_some_one(
- message.message_info.platform, message.message_info.user_info.user_id
- )
- if not is_known:
- logger.info(f"首次认识用户: {message.message_info.user_info.user_nickname}")
- await relationship_manager.first_knowing_some_one(
- message.message_info.platform,
- message.message_info.user_info.user_id,
- message.message_info.user_info.user_nickname,
- message.message_info.user_info.user_cardname or message.message_info.user_info.user_nickname,
- "",
- )
- else:
- # logger.debug(f"已认识用户: {message.message_info.user_info.user_nickname}")
- if not await relationship_manager.is_qved_name(
- message.message_info.platform, message.message_info.user_info.user_id
- ):
- logger.info(f"更新已认识但未取名的用户: {message.message_info.user_info.user_nickname}")
- await relationship_manager.first_knowing_some_one(
- message.message_info.platform,
- message.message_info.user_info.user_id,
- message.message_info.user_info.user_nickname,
- message.message_info.user_info.user_cardname
- or message.message_info.user_info.user_nickname,
- "",
- )
- except Exception as e:
- logger.error(f"处理认识关系失败: {e}")
- logger.error(traceback.format_exc())
+ # 8. 关系处理
+ await self._process_relationship(message)
except Exception as e:
- logger.error(f"消息处理失败 (process_message V3): {e}")
- logger.error(traceback.format_exc())
- if message: # 记录失败的消息内容
- logger.error(f"失败消息原始内容: {message.raw_message}")
+ await self._handle_error(e, "消息处理失败", message)
def _check_ban_words(self, text: str, chat, userinfo) -> bool:
- """检查消息中是否包含过滤词"""
+ """检查消息是否包含过滤词
+
+ Args:
+ text: 待检查的文本
+ chat: 聊天对象
+ userinfo: 用户信息
+
+ Returns:
+ bool: 是否包含过滤词
+ """
for word in global_config.ban_words:
if word in text:
- logger.info(
- f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}"
- )
+ chat_name = chat.group_info.group_name if chat.group_info else "私聊"
+ logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}")
logger.info(f"[过滤词识别]消息中含有{word},filtered")
return True
return False
def _check_ban_regex(self, text: str, chat, userinfo) -> bool:
- """检查消息是否匹配过滤正则表达式"""
+ """检查消息是否匹配过滤正则表达式
+
+ Args:
+ text: 待检查的文本
+ chat: 聊天对象
+ userinfo: 用户信息
+
+ Returns:
+ bool: 是否匹配过滤正则
+ """
for pattern in global_config.ban_msgs_regex:
if pattern.search(text):
- logger.info(
- f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}"
- )
+ chat_name = chat.group_info.group_name if chat.group_info else "私聊"
+ logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}")
logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered")
return True
return False
diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py
index e9148c4fc..4b5218ae1 100644
--- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py
+++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py
@@ -21,42 +21,80 @@ logger = get_module_logger("prompt")
def init_prompt():
Prompt(
"""
-你有以下信息可供参考:
-{structured_info}
+{info_from_tools}
{chat_target}
{chat_talking_prompt}
现在你想要在群里发言或者回复。\n
你需要扮演一位网名叫{bot_name}的人进行回复,这个人的特点是:"{prompt_personality} {prompt_identity}"。
-你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,你可以参考贴吧,小红书或者微博的回复风格。
-你刚刚脑子里在想:
+你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,你可以参考贴吧,知乎或者微博的回复风格。
+看到以上聊天记录,你刚刚在想:
+
{current_mind_info}
-{reason}
+因为上述想法,你决定发言,原因是:{reason}
+
回复尽量简短一些。请注意把握聊天内容,不要回复的太有条理,可以有个性。请一次只回复一个话题,不要同时回复多个人,不用指出你回复的是谁。{prompt_ger}
请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,不要说你说过的话题 ,注意只输出回复内容。
-{moderation_prompt}。注意:不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""",
+{moderation_prompt}。注意:回复不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""",
"heart_flow_prompt",
)
-
- # Planner提示词
+
Prompt(
- """你的名字是 {bot_name}。是一个女大二学生,正在QQ聊天,正在决定是否以及如何回应当前的聊天。
+ """
+你有以下信息可供参考:
+{structured_info}
+以上的消息是你获取到的消息,或许可以帮助你更好地回复。
+""",
+ "info_from_tools",
+ )
+
+ # Planner提示词 - 优化版
+ Prompt(
+ """你的名字是{bot_name},一名大二女生,正在QQ群聊中。需要基于以下信息决定如何参与对话:
{structured_info_block}
{chat_content_block}
-看了以上内容,你产生的内心想法是:
+你的内心想法:
{current_mind_block}
-请结合你的内心想法和观察到的聊天内容,分析情况并使用 'decide_reply_action' 工具来决定你的最终行动。
-决策依据:
-1. 如果聊天内容无聊、与你无关、或者你的内心想法认为不适合回复(例如在讨论你不懂或不感兴趣的话题),选择 'no_reply'。
-2. 如果聊天内容值得回应,且适合用文字表达(参考你的内心想法),选择 'text_reply'。如果你有情绪想表达,想在文字后追加一个表达情绪的表情,请同时提供 'emoji_query' (例如:'开心的'、'惊讶的')。
-3. 如果聊天内容或你的内心想法适合用一个表情来回应(例如表示赞同、惊讶、无语等),选择 'emoji_reply' 并提供表情主题 'emoji_query'。
-4. 如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,通常选择 'no_reply',除非有特殊原因需要追问。
-5. 如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等;。
-6. 表情包是用来表达情绪的,不要直接回复或评价别人的表情包,而是根据对话内容和情绪选择是否用表情回应。
-7. 不要回复你自己的话,不要把自己的话当做别人说的。
-必须调用 'decide_reply_action' 工具并提供 'action' 和 'reasoning'。如果选择了 'emoji_reply' 或者选择了 'text_reply' 并想追加表情,则必须提供 'emoji_query'。""",
+{replan}
+
+请综合分析聊天内容和你看到的新消息,参考内心想法,使用'decide_reply_action'工具做出决策。决策时请注意:
+
+【回复原则】
+1. 不回复(no_reply)适用:
+- 话题无关/无聊/不感兴趣
+- 最后一条消息是你自己发的且无人回应你
+- 讨论你不懂的专业话题
+- 你发送了太多消息,且无人回复
+
+2. 文字回复(text_reply)适用:
+- 有实质性内容需要表达
+- 有人提到你,但你还没有回应他
+- 可以追加emoji_query表达情绪(格式:情绪描述,如"俏皮的调侃")
+- 不要追加太多表情
+
+3. 纯表情回复(emoji_reply)适用:
+- 适合用表情回应的场景
+- 需提供明确的emoji_query
+
+4. 自我对话处理:
+- 如果是自己发的消息想继续,需自然衔接
+- 避免重复或评价自己的发言
+- 不要和自己聊天
+
+【必须遵守】
+- 遵守回复原则
+- 必须调用工具并包含action和reasoning
+- 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply)
+- 选择text_reply或emoji_reply时必须提供emoji_query
+- 保持回复自然,符合日常聊天习惯""",
"planner_prompt",
)
-
+
+ Prompt(
+ """你原本打算{action},因为:{reasoning}
+但是你看到了新的消息,你决定重新决定行动。""",
+ "replan_prompt",
+ )
+
Prompt("你正在qq群里聊天,下面是群里在聊的内容:", "chat_target_group1")
Prompt("和群里聊天", "chat_target_group2")
Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1")
@@ -79,9 +117,9 @@ def init_prompt():
你的网名叫{bot_name},有人也叫你{bot_other_names},{prompt_personality}。
你正在{chat_target_2},现在请你读读之前的聊天记录,{mood_prompt},然后给出日常且口语化的回复,平淡一些,
尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。{prompt_ger}
-请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话
+请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,不要浮夸,平淡一些 ,不要重复自己说过的话。
请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。
-{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""",
+{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。,只输出回复内容""",
"reasoning_prompt_main",
)
Prompt(
@@ -116,13 +154,14 @@ class PromptBuilder:
elif build_mode == "focus":
return await self._build_prompt_focus(
- reason, current_mind_info, structured_info, chat_stream,
+ reason,
+ current_mind_info,
+ structured_info,
+ chat_stream,
)
return None
- async def _build_prompt_focus(
- self, reason, current_mind_info, structured_info, chat_stream
- ) -> tuple[str, str]:
+ async def _build_prompt_focus(self, reason, current_mind_info, structured_info, chat_stream) -> tuple[str, str]:
individuality = Individuality.get_instance()
prompt_personality = individuality.get_prompt(type="personality", x_person=2, level=1)
prompt_identity = individuality.get_prompt(type="identity", x_person=2, level=1)
@@ -145,7 +184,7 @@ class PromptBuilder:
message_list_before_now,
replace_bot_name=True,
merge_messages=False,
- timestamp_mode="relative",
+ timestamp_mode="normal",
read_mark=0.0,
)
@@ -156,11 +195,18 @@ class PromptBuilder:
if random.random() < 0.02:
prompt_ger += "你喜欢用反问句"
+ if structured_info:
+ structured_info_prompt = await global_prompt_manager.format_prompt(
+ "info_from_tools", structured_info=structured_info
+ )
+ else:
+ structured_info_prompt = ""
+
logger.debug("开始构建prompt")
prompt = await global_prompt_manager.format_prompt(
"heart_flow_prompt",
- structured_info=structured_info,
+ info_from_tools=structured_info_prompt,
chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1")
if chat_in_group
else await global_prompt_manager.get_prompt_async("chat_target_private1"),
@@ -490,23 +536,36 @@ class PromptBuilder:
logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}")
# 从LPMM知识库获取知识
- found_knowledge_from_lpmm = qa_manager.get_knowledge(message)
+ try:
+ found_knowledge_from_lpmm = qa_manager.get_knowledge(message)
- end_time = time.time()
- if found_knowledge_from_lpmm is not None:
- logger.debug(
- f"从LPMM知识库获取知识,相关信息:{found_knowledge_from_lpmm[:100]}...,信息长度: {len(found_knowledge_from_lpmm)}"
- )
- related_info += found_knowledge_from_lpmm
- logger.debug(f"获取知识库内容耗时: {(end_time - start_time):.3f}秒")
- logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}")
- return related_info
- else:
- logger.debug("从LPMM知识库获取知识失败,使用旧版数据库进行检索")
- knowledge_from_old = await self.get_prompt_info_old(message, threshold=0.38)
- related_info += knowledge_from_old
- logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}")
- return related_info
+ end_time = time.time()
+ if found_knowledge_from_lpmm is not None:
+ logger.debug(
+ f"从LPMM知识库获取知识,相关信息:{found_knowledge_from_lpmm[:100]}...,信息长度: {len(found_knowledge_from_lpmm)}"
+ )
+ related_info += found_knowledge_from_lpmm
+ logger.debug(f"获取知识库内容耗时: {(end_time - start_time):.3f}秒")
+ logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}")
+ return related_info
+ else:
+ logger.debug("从LPMM知识库获取知识失败,使用旧版数据库进行检索")
+ knowledge_from_old = await self.get_prompt_info_old(message, threshold=0.38)
+ related_info += knowledge_from_old
+ logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}")
+ return related_info
+ except Exception as e:
+ logger.error(f"获取知识库内容时发生异常: {str(e)}")
+ try:
+ knowledge_from_old = await self.get_prompt_info_old(message, threshold=0.38)
+ related_info += knowledge_from_old
+ logger.debug(
+ f"异常后使用旧版数据库获取知识,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}"
+ )
+ return related_info
+ except Exception as e2:
+ logger.error(f"使用旧版数据库获取知识时也发生异常: {str(e2)}")
+ return ""
@staticmethod
def get_info_from_db(
diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py
index a3aaf3a0b..890b5a82a 100644
--- a/src/plugins/heartFC_chat/normal_chat.py
+++ b/src/plugins/heartFC_chat/normal_chat.py
@@ -1,23 +1,24 @@
import time
import asyncio
import traceback
+import statistics # 导入 statistics 模块
from random import random
from typing import List, Optional # 导入 Optional
from ..moods.moods import MoodManager
from ...config.config import global_config
-from ..chat.emoji_manager import emoji_manager
+from ..emoji_system.emoji_manager import emoji_manager
from .normal_chat_generator import NormalChatGenerator
from ..chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet
from ..chat.message_sender import message_manager
from ..chat.utils_image import image_path_to_base64
from ..willing.willing_manager import willing_manager
-from ..message import UserInfo, Seg
+from maim_message import UserInfo, Seg
from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig
from src.plugins.chat.chat_stream import ChatStream, chat_manager
from src.plugins.person_info.relationship_manager import relationship_manager
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
-from src.plugins.utils.timer_calculater import Timer
+from src.plugins.utils.timer_calculator import Timer
# 定义日志配置
chat_config = LogConfig(
@@ -46,6 +47,8 @@ class NormalChat:
self.gpt = NormalChatGenerator()
self.mood_manager = MoodManager.get_instance() # MoodManager 保持单例
# 存储此实例的兴趣监控任务
+ self.start_time = time.time()
+
self._chat_task: Optional[asyncio.Task] = None
logger.info(f"[{self.stream_name}] NormalChat 实例初始化完成。")
@@ -164,14 +167,13 @@ class NormalChat:
)
self.mood_manager.update_mood_from_emotion(emotion, global_config.mood_intensity_factor)
- async def _find_interested_message(self) -> None:
+ async def _reply_interested_message(self) -> None:
"""
后台任务方法,轮询当前实例关联chat的兴趣消息
通常由start_monitoring_interest()启动
"""
while True:
- await asyncio.sleep(1) # 每秒检查一次
-
+ await asyncio.sleep(0.5) # 每秒检查一次
# 检查任务是否已被取消
if self._chat_task is None or self._chat_task.cancelled():
logger.info(f"[{self.stream_name}] 兴趣监控任务被取消或置空,退出")
@@ -318,6 +320,68 @@ class NormalChat:
# 意愿管理器:注销当前message信息 (无论是否回复,只要处理过就删除)
willing_manager.delete(message.message_info.message_id)
+ # --- 新增:处理初始高兴趣消息的私有方法 ---
+ async def _process_initial_interest_messages(self):
+ """处理启动时存在于 interest_dict 中的高兴趣消息。"""
+ items_to_process = list(self.interest_dict.items())
+ if not items_to_process:
+ return # 没有初始消息,直接返回
+
+ logger.info(f"[{self.stream_name}] 发现 {len(items_to_process)} 条初始兴趣消息,开始处理高兴趣部分...")
+ interest_values = [item[1][1] for item in items_to_process] # 提取兴趣值列表
+
+ messages_to_reply = [] # 需要立即回复的消息
+
+ if len(interest_values) == 1:
+ # 如果只有一个消息,直接处理
+ messages_to_reply.append(items_to_process[0])
+ logger.info(f"[{self.stream_name}] 只有一条初始消息,直接处理。")
+ elif len(interest_values) > 1:
+ # 计算均值和标准差
+ try:
+ mean_interest = statistics.mean(interest_values)
+ stdev_interest = statistics.stdev(interest_values)
+ threshold = mean_interest + stdev_interest
+ logger.info(
+ f"[{self.stream_name}] 初始兴趣值 均值: {mean_interest:.2f}, 标准差: {stdev_interest:.2f}, 阈值: {threshold:.2f}"
+ )
+
+ # 找出高于阈值的消息
+ for item in items_to_process:
+ msg_id, (message, interest_value, is_mentioned) = item
+ if interest_value > threshold:
+ messages_to_reply.append(item)
+ logger.info(f"[{self.stream_name}] 找到 {len(messages_to_reply)} 条高于阈值的初始消息进行处理。")
+ except statistics.StatisticsError as e:
+ logger.error(f"[{self.stream_name}] 计算初始兴趣统计值时出错: {e},跳过初始处理。")
+
+ # 处理需要回复的消息
+ processed_count = 0
+ # --- 修改:迭代前创建要处理的ID列表副本,防止迭代时修改 ---
+ messages_to_process_initially = list(messages_to_reply) # 创建副本
+ # --- 修改结束 ---
+ for item in messages_to_process_initially: # 使用副本迭代
+ msg_id, (message, interest_value, is_mentioned) = item
+ # --- 修改:在处理前尝试 pop,防止竞争 ---
+ popped_item = self.interest_dict.pop(msg_id, None)
+ if popped_item is None:
+ logger.warning(f"[{self.stream_name}] 初始兴趣消息 {msg_id} 在处理前已被移除,跳过。")
+ continue # 如果消息已被其他任务处理(pop),则跳过
+ # --- 修改结束 ---
+
+ try:
+ logger.info(f"[{self.stream_name}] 处理初始高兴趣消息 {msg_id} (兴趣值: {interest_value:.2f})")
+ await self.normal_response(message=message, is_mentioned=is_mentioned, interested_rate=interest_value)
+ processed_count += 1
+ except Exception as e:
+ logger.error(f"[{self.stream_name}] 处理初始兴趣消息 {msg_id} 时出错: {e}\\n{traceback.format_exc()}")
+
+ logger.info(
+ f"[{self.stream_name}] 初始高兴趣消息处理完毕,共处理 {processed_count} 条。剩余 {len(self.interest_dict)} 条待轮询。"
+ )
+
+ # --- 新增结束 ---
+
# 保持 staticmethod, 因为不依赖实例状态, 但需要 chat 对象来获取日志上下文
@staticmethod
def _check_ban_words(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
@@ -351,38 +415,42 @@ class NormalChat:
# 改为实例方法, 移除 chat 参数
async def start_chat(self):
- """为此 NormalChat 实例关联的 ChatStream 启动聊天任务(如果尚未运行)。"""
+ """为此 NormalChat 实例关联的 ChatStream 启动聊天任务(如果尚未运行),
+ 并在后台处理一次初始的高兴趣消息。""" # 文言文注释示例:启聊之始,若有遗珠,当于暗处拂拭,勿碍正途。
if self._chat_task is None or self._chat_task.done():
- logger.info(f"[{self.stream_name}] 启动聊天任务...")
- task = asyncio.create_task(self._find_interested_message())
- task.add_done_callback(lambda t: self._handle_task_completion(t)) # 回调现在是实例方法
- self._chat_task = task
+ # --- 修改:使用 create_task 启动初始消息处理 ---
+ logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息...")
+ # 创建一个任务来处理初始消息,不阻塞当前流程
+ _initial_process_task = asyncio.create_task(self._process_initial_interest_messages())
+ # 可以考虑给这个任务也添加完成回调来记录日志或处理错误
+ # initial_process_task.add_done_callback(...)
+ # --- 修改结束 ---
+
+ # 启动后台轮询任务 (这部分不变)
+ logger.info(f"[{self.stream_name}] 启动后台兴趣消息轮询任务...")
+ polling_task = asyncio.create_task(self._reply_interested_message()) # 注意变量名区分
+ polling_task.add_done_callback(lambda t: self._handle_task_completion(t))
+ self._chat_task = polling_task # self._chat_task 仍然指向主要的轮询任务
+ else:
+ logger.info(f"[{self.stream_name}] 聊天轮询任务已在运行中。")
- # 改为实例方法, 移除 stream_id 参数
def _handle_task_completion(self, task: asyncio.Task):
- """兴趣监控任务完成时的回调函数。"""
- # 检查完成的任务是否是当前实例的任务
+ """任务完成回调处理"""
if task is not self._chat_task:
- logger.warning(f"[{self.stream_name}] 收到一个未知或过时任务的完成回调。")
+ logger.warning(f"[{self.stream_name}] 收到未知任务回调")
return
-
try:
- # 检查任务是否因异常而结束
- exception = task.exception()
- if exception:
- logger.error(f"[{self.stream_name}] 兴趣监控任务因异常结束: {exception}")
- logger.error(traceback.format_exc()) # 记录完整的 traceback
- # else: # 减少日志
- # logger.info(f"[{self.stream_name}] 兴趣监控任务正常结束。")
+ if exc := task.exception():
+ logger.error(f"[{self.stream_name}] 任务异常: {exc}")
+ logger.error(traceback.format_exc())
except asyncio.CancelledError:
- logger.info(f"[{self.stream_name}] 兴趣监控任务被取消。")
+ logger.info(f"[{self.stream_name}] 任务已取消")
except Exception as e:
- logger.error(f"[{self.stream_name}] 处理任务完成回调时出错: {e}")
+ logger.error(f"[{self.stream_name}] 回调处理错误: {e}")
finally:
- # 标记任务已完成/移除
- if self._chat_task is task: # 再次确认是当前任务
+ if self._chat_task is task:
self._chat_task = None
- logger.debug(f"[{self.stream_name}] 聊天任务已被标记为完成/移除。")
+ logger.debug(f"[{self.stream_name}] 任务清理完成")
# 改为实例方法, 移除 stream_id 参数
async def stop_chat(self):
@@ -402,7 +470,7 @@ class NormalChat:
# 确保任务状态更新,即使等待出错 (回调函数也会尝试更新)
if self._chat_task is task:
self._chat_task = None
-
+
# 清理所有未处理的思考消息
try:
container = await message_manager.get_container(self.stream_id)
diff --git a/src/plugins/heartFC_chat/normal_chat_generator.py b/src/plugins/heartFC_chat/normal_chat_generator.py
index cd9208b3a..52d0f446f 100644
--- a/src/plugins/heartFC_chat/normal_chat_generator.py
+++ b/src/plugins/heartFC_chat/normal_chat_generator.py
@@ -5,7 +5,7 @@ from ...config.config import global_config
from ..chat.message import MessageThinking
from .heartflow_prompt_builder import prompt_builder
from ..chat.utils import process_llm_response
-from ..utils.timer_calculater import Timer
+from ..utils.timer_calculator import Timer
from src.common.logger import get_module_logger, LogConfig, LLM_STYLE_CONFIG
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py
index 738e47c4e..1e8ad8850 100644
--- a/src/plugins/memory_system/Hippocampus.py
+++ b/src/plugins/memory_system/Hippocampus.py
@@ -404,7 +404,7 @@ class Hippocampus:
# logger.info("没有找到有效的关键词节点")
return []
- logger.info(f"有效的关键词: {', '.join(valid_keywords)}")
+ logger.debug(f"有效的关键词: {', '.join(valid_keywords)}")
# 从每个关键词获取记忆
all_memories = []
@@ -576,7 +576,7 @@ class Hippocampus:
# logger.info("没有找到有效的关键词节点")
return []
- logger.info(f"有效的关键词: {', '.join(valid_keywords)}")
+ logger.debug(f"有效的关键词: {', '.join(valid_keywords)}")
# 从每个关键词获取记忆
all_memories = []
@@ -761,7 +761,7 @@ class Hippocampus:
# logger.info("没有找到有效的关键词节点")
return 0
- logger.info(f"有效的关键词: {', '.join(valid_keywords)}")
+ logger.debug(f"有效的关键词: {', '.join(valid_keywords)}")
# 从每个关键词获取记忆
activate_map = {} # 存储每个词的累计激活值
diff --git a/src/plugins/message/__init__.py b/src/plugins/message/__init__.py
index 286ef2310..b5eed4d45 100644
--- a/src/plugins/message/__init__.py
+++ b/src/plugins/message/__init__.py
@@ -3,23 +3,8 @@
__version__ = "0.1.0"
from .api import global_api
-from .message_base import (
- Seg,
- GroupInfo,
- UserInfo,
- FormatInfo,
- TemplateInfo,
- BaseMessageInfo,
- MessageBase,
-)
+
__all__ = [
- "Seg",
"global_api",
- "GroupInfo",
- "UserInfo",
- "FormatInfo",
- "TemplateInfo",
- "BaseMessageInfo",
- "MessageBase",
]
diff --git a/src/plugins/message/api.py b/src/plugins/message/api.py
index fb51539e2..e82ab98fe 100644
--- a/src/plugins/message/api.py
+++ b/src/plugins/message/api.py
@@ -1,250 +1,6 @@
-from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
-from typing import Dict, Any, Callable, List, Set, Optional
-from src.common.logger import get_module_logger
-from src.plugins.message.message_base import MessageBase
from src.common.server import global_server
-import aiohttp
-import asyncio
-import uvicorn
import os
-import traceback
-
-logger = get_module_logger("api")
-
-
-class BaseMessageHandler:
- """消息处理基类"""
-
- def __init__(self):
- self.message_handlers: List[Callable] = []
- self.background_tasks = set()
-
- def register_message_handler(self, handler: Callable):
- """注册消息处理函数"""
- self.message_handlers.append(handler)
-
- async def process_message(self, message: Dict[str, Any]):
- """处理单条消息"""
- tasks = []
- for handler in self.message_handlers:
- try:
- tasks.append(handler(message))
- except Exception as e:
- logger.error(f"消息处理出错: {str(e)}")
- logger.error(traceback.format_exc())
- # 不抛出异常,而是记录错误并继续处理其他消息
- continue
- if tasks:
- await asyncio.gather(*tasks, return_exceptions=True)
-
- async def _handle_message(self, message: Dict[str, Any]):
- """后台处理单个消息"""
- try:
- await self.process_message(message)
- except Exception as e:
- raise RuntimeError(str(e)) from e
-
-
-class MessageServer(BaseMessageHandler):
- """WebSocket服务端"""
-
- _class_handlers: List[Callable] = [] # 类级别的消息处理器
-
- def __init__(
- self,
- host: str = "0.0.0.0",
- port: int = 18000,
- enable_token=False,
- app: Optional[FastAPI] = None,
- path: str = "/ws",
- ):
- super().__init__()
- # 将类级别的处理器添加到实例处理器中
- self.message_handlers.extend(self._class_handlers)
- self.host = host
- self.port = port
- self.path = path
- self.app = app or FastAPI()
- self.own_app = app is None # 标记是否使用自己创建的app
- self.active_websockets: Set[WebSocket] = set()
- self.platform_websockets: Dict[str, WebSocket] = {} # 平台到websocket的映射
- self.valid_tokens: Set[str] = set()
- self.enable_token = enable_token
- self._setup_routes()
- self._running = False
-
- def _setup_routes(self):
- @self.app.post("/api/message")
- async def handle_message(message: Dict[str, Any]):
- try:
- # 创建后台任务处理消息
- asyncio.create_task(self._handle_message(message))
- return {"status": "success"}
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e)) from e
-
- @self.app.websocket("/ws")
- async def websocket_endpoint(websocket: WebSocket):
- headers = dict(websocket.headers)
- token = headers.get("authorization")
- platform = headers.get("platform", "default") # 获取platform标识
- if self.enable_token:
- if not token or not await self.verify_token(token):
- await websocket.close(code=1008, reason="Invalid or missing token")
- return
-
- await websocket.accept()
- self.active_websockets.add(websocket)
-
- # 添加到platform映射
- if platform not in self.platform_websockets:
- self.platform_websockets[platform] = websocket
-
- try:
- while True:
- message = await websocket.receive_json()
- # print(f"Received message: {message}")
- asyncio.create_task(self._handle_message(message))
- except WebSocketDisconnect:
- self._remove_websocket(websocket, platform)
- except Exception as e:
- self._remove_websocket(websocket, platform)
- raise RuntimeError(str(e)) from e
- finally:
- self._remove_websocket(websocket, platform)
-
- @classmethod
- def register_class_handler(cls, handler: Callable):
- """注册类级别的消息处理器"""
- if handler not in cls._class_handlers:
- cls._class_handlers.append(handler)
-
- def register_message_handler(self, handler: Callable):
- """注册实例级别的消息处理器"""
- if handler not in self.message_handlers:
- self.message_handlers.append(handler)
-
- async def verify_token(self, token: str) -> bool:
- if not self.enable_token:
- return True
- return token in self.valid_tokens
-
- def add_valid_token(self, token: str):
- self.valid_tokens.add(token)
-
- def remove_valid_token(self, token: str):
- self.valid_tokens.discard(token)
-
- def run_sync(self):
- """同步方式运行服务器"""
- if not self.own_app:
- raise RuntimeError("当使用外部FastAPI实例时,请使用该实例的运行方法")
- uvicorn.run(self.app, host=self.host, port=self.port)
-
- async def run(self):
- """异步方式运行服务器"""
- self._running = True
- try:
- if self.own_app:
- # 如果使用自己的 FastAPI 实例,运行 uvicorn 服务器
- # 禁用 uvicorn 默认日志和访问日志
- config = uvicorn.Config(
- self.app, host=self.host, port=self.port, loop="asyncio", log_config=None, access_log=False
- )
- self.server = uvicorn.Server(config)
- await self.server.serve()
- else:
- # 如果使用外部 FastAPI 实例,保持运行状态以处理消息
- while self._running:
- await asyncio.sleep(1)
- except KeyboardInterrupt:
- await self.stop()
- raise
- except Exception as e:
- await self.stop()
- raise RuntimeError(f"服务器运行错误: {str(e)}") from e
- finally:
- await self.stop()
-
- async def start_server(self):
- """启动服务器的异步方法"""
- if not self._running:
- self._running = True
- await self.run()
-
- async def stop(self):
- """停止服务器"""
- # 清理platform映射
- self.platform_websockets.clear()
-
- # 取消所有后台任务
- for task in self.background_tasks:
- task.cancel()
- # 等待所有任务完成
- await asyncio.gather(*self.background_tasks, return_exceptions=True)
- self.background_tasks.clear()
-
- # 关闭所有WebSocket连接
- for websocket in self.active_websockets:
- await websocket.close()
- self.active_websockets.clear()
-
- if hasattr(self, "server") and self.own_app:
- self._running = False
- # 正确关闭 uvicorn 服务器
- self.server.should_exit = True
- await self.server.shutdown()
- # 等待服务器完全停止
- if hasattr(self.server, "started") and self.server.started:
- await self.server.main_loop()
- # 清理处理程序
- self.message_handlers.clear()
-
- def _remove_websocket(self, websocket: WebSocket, platform: str):
- """从所有集合中移除websocket"""
- if websocket in self.active_websockets:
- self.active_websockets.remove(websocket)
- if platform in self.platform_websockets:
- if self.platform_websockets[platform] == websocket:
- del self.platform_websockets[platform]
-
- async def broadcast_message(self, message: Dict[str, Any]):
- disconnected = set()
- for websocket in self.active_websockets:
- try:
- await websocket.send_json(message)
- except Exception:
- disconnected.add(websocket)
- for websocket in disconnected:
- self.active_websockets.remove(websocket)
-
- async def broadcast_to_platform(self, platform: str, message: Dict[str, Any]):
- """向指定平台的所有WebSocket客户端广播消息"""
- if platform not in self.platform_websockets:
- raise ValueError(f"平台:{platform} 未连接")
-
- disconnected = set()
- try:
- await self.platform_websockets[platform].send_json(message)
- except Exception:
- disconnected.add(self.platform_websockets[platform])
-
- # 清理断开的连接
- for websocket in disconnected:
- self._remove_websocket(websocket, platform)
-
- async def send_message(self, message: MessageBase):
- await self.broadcast_to_platform(message.message_info.platform, message.to_dict())
-
- @staticmethod
- async def send_message_rest(url: str, data: Dict[str, Any]) -> Dict[str, Any]:
- """发送消息到指定端点"""
- async with aiohttp.ClientSession() as session:
- try:
- async with session.post(url, json=data, headers={"Content-Type": "application/json"}) as response:
- return await response.json()
- except Exception as e:
- raise e
+from maim_message import MessageServer
global_api = MessageServer(host=os.environ["HOST"], port=int(os.environ["PORT"]), app=global_server.get_app())
diff --git a/src/plugins/message/message_base.py b/src/plugins/message/message_base.py
deleted file mode 100644
index b853d469a..000000000
--- a/src/plugins/message/message_base.py
+++ /dev/null
@@ -1,247 +0,0 @@
-from dataclasses import dataclass, asdict
-from typing import List, Optional, Union, Dict
-
-
-@dataclass
-class Seg:
- """消息片段类,用于表示消息的不同部分
-
- Attributes:
- type: 片段类型,可以是 'text'、'image'、'seglist' 等
- data: 片段的具体内容
- - 对于 text 类型,data 是字符串
- - 对于 image 类型,data 是 base64 字符串
- - 对于 seglist 类型,data 是 Seg 列表
- """
-
- type: str
- data: Union[str, List["Seg"]]
-
- # def __init__(self, type: str, data: Union[str, List['Seg']],):
- # """初始化实例,确保字典和属性同步"""
- # # 先初始化字典
- # self.type = type
- # self.data = data
-
- @classmethod
- def from_dict(cls, data: Dict) -> "Seg":
- """从字典创建Seg实例"""
- type = data.get("type")
- data = data.get("data")
- if type == "seglist":
- data = [Seg.from_dict(seg) for seg in data]
- return cls(type=type, data=data)
-
- def to_dict(self) -> Dict:
- """转换为字典格式"""
- result = {"type": self.type}
- if self.type == "seglist":
- result["data"] = [seg.to_dict() for seg in self.data]
- else:
- result["data"] = self.data
- return result
-
-
-@dataclass
-class GroupInfo:
- """群组信息类"""
-
- platform: Optional[str] = None
- group_id: Optional[int] = None
- group_name: Optional[str] = None # 群名称
-
- def to_dict(self) -> Dict:
- """转换为字典格式"""
- return {k: v for k, v in asdict(self).items() if v is not None}
-
- @classmethod
- def from_dict(cls, data: Dict) -> "GroupInfo":
- """从字典创建GroupInfo实例
-
- Args:
- data: 包含必要字段的字典
-
- Returns:
- GroupInfo: 新的实例
- """
- if data.get("group_id") is None:
- return None
- return cls(
- platform=data.get("platform"), group_id=data.get("group_id"), group_name=data.get("group_name", None)
- )
-
-
-@dataclass
-class UserInfo:
- """用户信息类"""
-
- platform: Optional[str] = None
- user_id: Optional[int] = None
- user_nickname: Optional[str] = None # 用户昵称
- user_cardname: Optional[str] = None # 用户群昵称
-
- def to_dict(self) -> Dict:
- """转换为字典格式"""
- return {k: v for k, v in asdict(self).items() if v is not None}
-
- @classmethod
- def from_dict(cls, data: Dict) -> "UserInfo":
- """从字典创建UserInfo实例
-
- Args:
- data: 包含必要字段的字典
-
- Returns:
- UserInfo: 新的实例
- """
- return cls(
- platform=data.get("platform"),
- user_id=data.get("user_id"),
- user_nickname=data.get("user_nickname", None),
- user_cardname=data.get("user_cardname", None),
- )
-
-
-@dataclass
-class FormatInfo:
- """格式信息类"""
-
- """
- 目前maimcore可接受的格式为text,image,emoji
- 可发送的格式为text,emoji,reply
- """
-
- content_format: Optional[str] = None
- accept_format: Optional[str] = None
-
- def to_dict(self) -> Dict:
- """转换为字典格式"""
- return {k: v for k, v in asdict(self).items() if v is not None}
-
- @classmethod
- def from_dict(cls, data: Dict) -> "FormatInfo":
- """从字典创建FormatInfo实例
- Args:
- data: 包含必要字段的字典
- Returns:
- FormatInfo: 新的实例
- """
- return cls(
- content_format=data.get("content_format"),
- accept_format=data.get("accept_format"),
- )
-
-
-@dataclass
-class TemplateInfo:
- """模板信息类"""
-
- template_items: Optional[Dict] = None
- template_name: Optional[str] = None
- template_default: bool = True
-
- def to_dict(self) -> Dict:
- """转换为字典格式"""
- return {k: v for k, v in asdict(self).items() if v is not None}
-
- @classmethod
- def from_dict(cls, data: Dict) -> "TemplateInfo":
- """从字典创建TemplateInfo实例
- Args:
- data: 包含必要字段的字典
- Returns:
- TemplateInfo: 新的实例
- """
- return cls(
- template_items=data.get("template_items"),
- template_name=data.get("template_name"),
- template_default=data.get("template_default", True),
- )
-
-
-@dataclass
-class BaseMessageInfo:
- """消息信息类"""
-
- platform: Optional[str] = None
- message_id: Union[str, int, None] = None
- time: Optional[float] = None
- group_info: Optional[GroupInfo] = None
- user_info: Optional[UserInfo] = None
- format_info: Optional[FormatInfo] = None
- template_info: Optional[TemplateInfo] = None
- additional_config: Optional[dict] = None
-
- def to_dict(self) -> Dict:
- """转换为字典格式"""
- result = {}
- for field, value in asdict(self).items():
- if value is not None:
- if isinstance(value, (GroupInfo, UserInfo, FormatInfo, TemplateInfo)):
- result[field] = value.to_dict()
- else:
- result[field] = value
- return result
-
- @classmethod
- def from_dict(cls, data: Dict) -> "BaseMessageInfo":
- """从字典创建BaseMessageInfo实例
-
- Args:
- data: 包含必要字段的字典
-
- Returns:
- BaseMessageInfo: 新的实例
- """
- group_info = GroupInfo.from_dict(data.get("group_info", {}))
- user_info = UserInfo.from_dict(data.get("user_info", {}))
- format_info = FormatInfo.from_dict(data.get("format_info", {}))
- template_info = TemplateInfo.from_dict(data.get("template_info", {}))
- return cls(
- platform=data.get("platform"),
- message_id=data.get("message_id"),
- time=data.get("time"),
- additional_config=data.get("additional_config", None),
- group_info=group_info,
- user_info=user_info,
- format_info=format_info,
- template_info=template_info,
- )
-
-
-@dataclass
-class MessageBase:
- """消息类"""
-
- message_info: BaseMessageInfo
- message_segment: Seg
- raw_message: Optional[str] = None # 原始消息,包含未解析的cq码
-
- def to_dict(self) -> Dict:
- """转换为字典格式
-
- Returns:
- Dict: 包含所有非None字段的字典,其中:
- - message_info: 转换为字典格式
- - message_segment: 转换为字典格式
- - raw_message: 如果存在则包含
- """
- result = {"message_info": self.message_info.to_dict(), "message_segment": self.message_segment.to_dict()}
- if self.raw_message is not None:
- result["raw_message"] = self.raw_message
- return result
-
- @classmethod
- def from_dict(cls, data: Dict) -> "MessageBase":
- """从字典创建MessageBase实例
-
- Args:
- data: 包含必要字段的字典
-
- Returns:
- MessageBase: 新的实例
- """
- message_info = BaseMessageInfo.from_dict(data.get("message_info", {}))
- message_segment = Seg.from_dict(data.get("message_segment", {}))
- raw_message = data.get("raw_message", None)
- return cls(message_info=message_info, message_segment=message_segment, raw_message=raw_message)
diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py
index 2cab7b629..7c87cf946 100644
--- a/src/plugins/models/utils_model.py
+++ b/src/plugins/models/utils_model.py
@@ -178,395 +178,6 @@ class LLMRequest:
output_cost = (completion_tokens / 1000000) * self.pri_out
return round(input_cost + output_cost, 6)
- '''
- async def _execute_request(
- self,
- endpoint: str,
- prompt: str = None,
- image_base64: str = None,
- image_format: str = None,
- payload: dict = None,
- retry_policy: dict = None,
- response_handler: callable = None,
- user_id: str = "system",
- request_type: str = None,
- ):
- """统一请求执行入口
- Args:
- endpoint: API端点路径 (如 "chat/completions")
- prompt: prompt文本
- image_base64: 图片的base64编码
- image_format: 图片格式
- payload: 请求体数据
- retry_policy: 自定义重试策略
- response_handler: 自定义响应处理器
- user_id: 用户ID
- request_type: 请求类型
- """
-
- if request_type is None:
- request_type = self.request_type
-
- # 合并重试策略
- default_retry = {
- "max_retries": 3,
- "base_wait": 10,
- "retry_codes": [429, 413, 500, 503],
- "abort_codes": [400, 401, 402, 403],
- }
- policy = {**default_retry, **(retry_policy or {})}
-
- # 常见Error Code Mapping
- error_code_mapping = {
- 400: "参数不正确",
- 401: "API key 错误,认证失败,请检查/config/bot_config.toml和.env中的配置是否正确哦~",
- 402: "账号余额不足",
- 403: "需要实名,或余额不足",
- 404: "Not Found",
- 429: "请求过于频繁,请稍后再试",
- 500: "服务器内部故障",
- 503: "服务器负载过高",
- }
-
- api_url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}"
- # 判断是否为流式
- stream_mode = self.stream
- # logger_msg = "进入流式输出模式," if stream_mode else ""
- # logger.debug(f"{logger_msg}发送请求到URL: {api_url}")
- # logger.info(f"使用模型: {self.model_name}")
-
- # 构建请求体
- if image_base64:
- payload = await self._build_payload(prompt, image_base64, image_format)
- elif payload is None:
- payload = await self._build_payload(prompt)
-
- # 流式输出标志
- # 先构建payload,再添加流式输出标志
- if stream_mode:
- payload["stream"] = stream_mode
-
- for retry in range(policy["max_retries"]):
- try:
- # 使用上下文管理器处理会话
- headers = await self._build_headers()
- # 似乎是openai流式必须要的东西,不过阿里云的qwq-plus加了这个没有影响
- if stream_mode:
- headers["Accept"] = "text/event-stream"
-
- async with aiohttp.ClientSession() as session:
- try:
- async with session.post(api_url, headers=headers, json=payload) as response:
- # 处理需要重试的状态码
- if response.status in policy["retry_codes"]:
- wait_time = policy["base_wait"] * (2**retry)
- logger.warning(
- f"模型 {self.model_name} 错误码: {response.status}, 等待 {wait_time}秒后重试"
- )
- if response.status == 413:
- logger.warning("请求体过大,尝试压缩...")
- image_base64 = compress_base64_image_by_scale(image_base64)
- payload = await self._build_payload(prompt, image_base64, image_format)
- elif response.status in [500, 503]:
- logger.error(
- f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}"
- )
- raise RuntimeError("服务器负载过高,模型恢复失败QAQ")
- else:
- logger.warning(f"模型 {self.model_name} 请求限制(429),等待{wait_time}秒后重试...")
-
- await asyncio.sleep(wait_time)
- continue
- elif response.status in policy["abort_codes"]:
- logger.error(
- f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}"
- )
- # 尝试获取并记录服务器返回的详细错误信息
- try:
- error_json = await response.json()
- if error_json and isinstance(error_json, list) and len(error_json) > 0:
- for error_item in error_json:
- if "error" in error_item and isinstance(error_item["error"], dict):
- error_obj = error_item["error"]
- error_code = error_obj.get("code")
- error_message = error_obj.get("message")
- error_status = error_obj.get("status")
- logger.error(
- f"服务器错误详情: 代码={error_code}, 状态={error_status}, "
- f"消息={error_message}"
- )
- elif isinstance(error_json, dict) and "error" in error_json:
- # 处理单个错误对象的情况
- error_obj = error_json.get("error", {})
- error_code = error_obj.get("code")
- error_message = error_obj.get("message")
- error_status = error_obj.get("status")
- logger.error(
- f"服务器错误详情: 代码={error_code}, 状态={error_status}, 消息={error_message}"
- )
- else:
- # 记录原始错误响应内容
- logger.error(f"服务器错误响应: {error_json}")
- except Exception as e:
- logger.warning(f"无法解析服务器错误响应: {str(e)}")
-
- if response.status == 403:
- # 只针对硅基流动的V3和R1进行降级处理
- if (
- self.model_name.startswith("Pro/deepseek-ai")
- and self.base_url == "https://api.siliconflow.cn/v1/"
- ):
- old_model_name = self.model_name
- self.model_name = self.model_name[4:] # 移除"Pro/"前缀
- logger.warning(
- f"检测到403错误,模型从 {old_model_name} 降级为 {self.model_name}"
- )
-
- # 对全局配置进行更新
- if global_config.llm_normal.get("name") == old_model_name:
- global_config.llm_normal["name"] = self.model_name
- logger.warning(f"将全局配置中的 llm_normal 模型临时降级至{self.model_name}")
-
- if global_config.llm_reasoning.get("name") == old_model_name:
- global_config.llm_reasoning["name"] = self.model_name
- logger.warning(
- f"将全局配置中的 llm_reasoning 模型临时降级至{self.model_name}"
- )
-
- # 更新payload中的模型名
- if payload and "model" in payload:
- payload["model"] = self.model_name
-
- # 重新尝试请求
- retry -= 1 # 不计入重试次数
- continue
-
- raise RuntimeError(f"请求被拒绝: {error_code_mapping.get(response.status)}")
-
- response.raise_for_status()
- reasoning_content = ""
-
- # 将流式输出转化为非流式输出
- if stream_mode:
- flag_delta_content_finished = False
- accumulated_content = ""
- usage = None # 初始化usage变量,避免未定义错误
-
- async for line_bytes in response.content:
- try:
- line = line_bytes.decode("utf-8").strip()
- if not line:
- continue
- if line.startswith("data:"):
- data_str = line[5:].strip()
- if data_str == "[DONE]":
- break
- try:
- chunk = json.loads(data_str)
- if flag_delta_content_finished:
- chunk_usage = chunk.get("usage", None)
- if chunk_usage:
- usage = chunk_usage # 获取token用量
- else:
- delta = chunk["choices"][0]["delta"]
- delta_content = delta.get("content")
- if delta_content is None:
- delta_content = ""
- accumulated_content += delta_content
- # 检测流式输出文本是否结束
- finish_reason = chunk["choices"][0].get("finish_reason")
- if delta.get("reasoning_content", None):
- reasoning_content += delta["reasoning_content"]
- if finish_reason == "stop":
- chunk_usage = chunk.get("usage", None)
- if chunk_usage:
- usage = chunk_usage
- break
- # 部分平台在文本输出结束前不会返回token用量,此时需要再获取一次chunk
- flag_delta_content_finished = True
-
- except Exception as e:
- logger.exception(f"模型 {self.model_name} 解析流式输出错误: {str(e)}")
- except GeneratorExit:
- logger.warning("模型 {self.model_name} 流式输出被中断,正在清理资源...")
- # 确保资源被正确清理
- await response.release()
- # 返回已经累积的内容
- result = {
- "choices": [
- {
- "message": {
- "content": accumulated_content,
- "reasoning_content": reasoning_content,
- # 流式输出可能没有工具调用,此处不需要添加tool_calls字段
- }
- }
- ],
- "usage": usage,
- }
- return (
- response_handler(result)
- if response_handler
- else self._default_response_handler(result, user_id, request_type, endpoint)
- )
- except Exception as e:
- logger.error(f"模型 {self.model_name} 处理流式输出时发生错误: {str(e)}")
- # 确保在发生错误时也能正确清理资源
- try:
- await response.release()
- except Exception as cleanup_error:
- logger.error(f"清理资源时发生错误: {cleanup_error}")
- # 返回已经累积的内容
- result = {
- "choices": [
- {
- "message": {
- "content": accumulated_content,
- "reasoning_content": reasoning_content,
- # 流式输出可能没有工具调用,此处不需要添加tool_calls字段
- }
- }
- ],
- "usage": usage,
- }
- return (
- response_handler(result)
- if response_handler
- else self._default_response_handler(result, user_id, request_type, endpoint)
- )
- content = accumulated_content
- think_match = re.search(r"(.*?)", content, re.DOTALL)
- if think_match:
- reasoning_content = think_match.group(1).strip()
- content = re.sub(r".*?", "", content, flags=re.DOTALL).strip()
- # 构造一个伪result以便调用自定义响应处理器或默认处理器
- result = {
- "choices": [
- {
- "message": {
- "content": content,
- "reasoning_content": reasoning_content,
- # 流式输出可能没有工具调用,此处不需要添加tool_calls字段
- }
- }
- ],
- "usage": usage,
- }
- return (
- response_handler(result)
- if response_handler
- else self._default_response_handler(result, user_id, request_type, endpoint)
- )
- else:
- result = await response.json()
- # 使用自定义处理器或默认处理
- return (
- response_handler(result)
- if response_handler
- else self._default_response_handler(result, user_id, request_type, endpoint)
- )
-
- except (aiohttp.ClientError, asyncio.TimeoutError) as e:
- if retry < policy["max_retries"] - 1:
- wait_time = policy["base_wait"] * (2**retry)
- logger.error(f"模型 {self.model_name} 网络错误,等待{wait_time}秒后重试... 错误: {str(e)}")
- await asyncio.sleep(wait_time)
- continue
- else:
- logger.critical(f"模型 {self.model_name} 网络错误达到最大重试次数: {str(e)}")
- raise RuntimeError(f"网络请求失败: {str(e)}") from e
- except Exception as e:
- logger.critical(f"模型 {self.model_name} 未预期的错误: {str(e)}")
- raise RuntimeError(f"请求过程中发生错误: {str(e)}") from e
-
- except aiohttp.ClientResponseError as e:
- # 处理aiohttp抛出的响应错误
- if retry < policy["max_retries"] - 1:
- wait_time = policy["base_wait"] * (2**retry)
- logger.error(
- f"模型 {self.model_name} HTTP响应错误,等待{wait_time}秒后重试... 状态码: {e.status}, 错误: {e.message}"
- )
- try:
- if hasattr(e, "response") and e.response and hasattr(e.response, "text"):
- error_text = await e.response.text()
- try:
- error_json = json.loads(error_text)
- if isinstance(error_json, list) and len(error_json) > 0:
- for error_item in error_json:
- if "error" in error_item and isinstance(error_item["error"], dict):
- error_obj = error_item["error"]
- logger.error(
- f"模型 {self.model_name} 服务器错误详情: 代码={error_obj.get('code')}, "
- f"状态={error_obj.get('status')}, "
- f"消息={error_obj.get('message')}"
- )
- elif isinstance(error_json, dict) and "error" in error_json:
- error_obj = error_json.get("error", {})
- logger.error(
- f"模型 {self.model_name} 服务器错误详情: 代码={error_obj.get('code')}, "
- f"状态={error_obj.get('status')}, "
- f"消息={error_obj.get('message')}"
- )
- else:
- logger.error(f"模型 {self.model_name} 服务器错误响应: {error_json}")
- except (json.JSONDecodeError, TypeError) as json_err:
- logger.warning(
- f"模型 {self.model_name} 响应不是有效的JSON: {str(json_err)}, 原始内容: {error_text[:200]}"
- )
- except (AttributeError, TypeError, ValueError) as parse_err:
- logger.warning(f"模型 {self.model_name} 无法解析响应错误内容: {str(parse_err)}")
-
- await asyncio.sleep(wait_time)
- else:
- logger.critical(
- f"模型 {self.model_name} HTTP响应错误达到最大重试次数: 状态码: {e.status}, 错误: {e.message}"
- )
- # 安全地检查和记录请求详情
- if (
- image_base64
- and payload
- and isinstance(payload, dict)
- and "messages" in payload
- and len(payload["messages"]) > 0
- ):
- if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]:
- content = payload["messages"][0]["content"]
- if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]:
- payload["messages"][0]["content"][1]["image_url"]["url"] = (
- f"data:image/{image_format.lower() if image_format else 'jpeg'};base64,"
- f"{image_base64[:10]}...{image_base64[-10:]}"
- )
- logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {payload}")
- raise RuntimeError(f"模型 {self.model_name} API请求失败: 状态码 {e.status}, {e.message}") from e
- except Exception as e:
- if retry < policy["max_retries"] - 1:
- wait_time = policy["base_wait"] * (2**retry)
- logger.error(f"模型 {self.model_name} 请求失败,等待{wait_time}秒后重试... 错误: {str(e)}")
- await asyncio.sleep(wait_time)
- else:
- logger.critical(f"模型 {self.model_name} 请求失败: {str(e)}")
- # 安全地检查和记录请求详情
- if (
- image_base64
- and payload
- and isinstance(payload, dict)
- and "messages" in payload
- and len(payload["messages"]) > 0
- ):
- if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]:
- content = payload["messages"][0]["content"]
- if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]:
- payload["messages"][0]["content"][1]["image_url"]["url"] = (
- f"data:image/{image_format.lower() if image_format else 'jpeg'};base64,"
- f"{image_base64[:10]}...{image_base64[-10:]}"
- )
- logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {payload}")
- raise RuntimeError(f"模型 {self.model_name} API请求失败: {str(e)}") from e
-
- logger.error(f"模型 {self.model_name} 达到最大重试次数,请求仍然失败")
- raise RuntimeError(f"模型 {self.model_name} 达到最大重试次数,API请求仍然失败")
- '''
-
async def _prepare_request(
self,
endpoint: str,
@@ -820,6 +431,7 @@ class LLMRequest:
policy = request_content["policy"]
payload = request_content["payload"]
wait_time = policy["base_wait"] * (2**retry_count)
+ keep_request = False
if retry_count < policy["max_retries"] - 1:
keep_request = True
if isinstance(exception, RequestAbortException):
diff --git a/src/plugins/moods/moods.py b/src/plugins/moods/moods.py
index e3fb377c6..eea2177ff 100644
--- a/src/plugins/moods/moods.py
+++ b/src/plugins/moods/moods.py
@@ -256,7 +256,7 @@ class MoodManager:
def print_mood_status(self) -> None:
"""打印当前情绪状态"""
logger.info(
- f"[情绪状态]愉悦度: {self.current_mood.valence:.2f}, "
+ f"愉悦度: {self.current_mood.valence:.2f}, "
f"唤醒度: {self.current_mood.arousal:.2f}, "
f"心情: {self.current_mood.text}"
)
diff --git a/src/plugins/person_info/person_info.py b/src/plugins/person_info/person_info.py
index e4f4004e8..1ec9f6d06 100644
--- a/src/plugins/person_info/person_info.py
+++ b/src/plugins/person_info/person_info.py
@@ -53,7 +53,7 @@ person_info_default = {
# "impression" : None,
# "gender" : Unkown,
"konw_time": 0,
- "msg_interval": 3000,
+ "msg_interval": 2000,
"msg_interval_list": [],
} # 个人信息的各项与默认值在此定义,以下处理会自动创建/补全每一项
@@ -384,18 +384,30 @@ class PersonInfoManager:
if delta > 0:
time_interval.append(delta)
- time_interval = [t for t in time_interval if 500 <= t <= 8000]
- if len(time_interval) >= 30:
+ time_interval = [t for t in time_interval if 200 <= t <= 8000]
+ # --- 修改后的逻辑 ---
+ # 数据量检查 (至少需要 30 条有效间隔,并且足够进行头尾截断)
+ if len(time_interval) >= 30 + 10: # 至少30条有效+头尾各5条
time_interval.sort()
- # 画图(log)
+ # 画图(log) - 这部分保留
msg_interval_map = True
log_dir = Path("logs/person_info")
log_dir.mkdir(parents=True, exist_ok=True)
plt.figure(figsize=(10, 6))
- time_series = pd.Series(time_interval)
- plt.hist(time_series, bins=50, density=True, alpha=0.4, color="pink", label="Histogram")
- time_series.plot(kind="kde", color="mediumpurple", linewidth=1, label="Density")
+ # 使用截断前的数据画图,更能反映原始分布
+ time_series_original = pd.Series(time_interval)
+ plt.hist(
+ time_series_original,
+ bins=50,
+ density=True,
+ alpha=0.4,
+ color="pink",
+ label="Histogram (Original Filtered)",
+ )
+ time_series_original.plot(
+ kind="kde", color="mediumpurple", linewidth=1, label="Density (Original Filtered)"
+ )
plt.grid(True, alpha=0.2)
plt.xlim(0, 8000)
plt.title(f"Message Interval Distribution (User: {person_id[:8]}...)")
@@ -405,15 +417,24 @@ class PersonInfoManager:
img_path = log_dir / f"interval_distribution_{person_id[:8]}.png"
plt.savefig(img_path)
plt.close()
- # 画图
+ # 画图结束
- q25, q75 = np.percentile(time_interval, [25, 75])
- iqr = q75 - q25
- filtered = [x for x in time_interval if (q25 - 1.5 * iqr) <= x <= (q75 + 1.5 * iqr)]
+ # 去掉头尾各 5 个数据点
+ trimmed_interval = time_interval[5:-5]
- msg_interval = int(round(np.percentile(filtered, 80)))
- await self.update_one_field(person_id, "msg_interval", msg_interval)
- logger.trace(f"用户{person_id}的msg_interval已经被更新为{msg_interval}")
+ # 计算截断后数据的 37% 分位数
+ if trimmed_interval: # 确保截断后列表不为空
+ msg_interval = int(round(np.percentile(trimmed_interval, 37)))
+ # 更新数据库
+ await self.update_one_field(person_id, "msg_interval", msg_interval)
+ logger.trace(f"用户{person_id}的msg_interval通过头尾截断和37分位数更新为{msg_interval}")
+ else:
+ logger.trace(f"用户{person_id}截断后数据为空,无法计算msg_interval")
+ else:
+ logger.trace(
+ f"用户{person_id}有效消息间隔数量 ({len(time_interval)}) 不足进行推断 (需要至少 {30 + 10} 条)"
+ )
+ # --- 修改结束 ---
except Exception as e:
logger.trace(f"用户{person_id}消息间隔计算失败: {type(e).__name__}: {str(e)}")
continue
diff --git a/src/plugins/utils/chat_message_builder.py b/src/plugins/utils/chat_message_builder.py
index 6ae6ccc32..f510365fa 100644
--- a/src/plugins/utils/chat_message_builder.py
+++ b/src/plugins/utils/chat_message_builder.py
@@ -168,7 +168,10 @@ async def _build_readable_messages_internal(
user_info = msg.get("user_info", {})
platform = user_info.get("platform")
user_id = user_info.get("user_id")
- user_nickname = user_info.get("nickname")
+
+ user_nickname = user_info.get("user_nickname")
+ user_cardname = user_info.get("user_cardname")
+
timestamp = msg.get("time")
content = msg.get("processed_plain_text", "") # 默认空字符串
@@ -186,7 +189,12 @@ async def _build_readable_messages_internal(
# 如果 person_name 未设置,则使用消息中的 nickname 或默认名称
if not person_name:
- person_name = user_nickname
+ if user_cardname:
+ person_name = f"昵称:{user_cardname}"
+ elif user_nickname:
+ person_name = f"{user_nickname}"
+ else:
+ person_name = "某人"
message_details.append((timestamp, person_name, content))
@@ -303,9 +311,7 @@ async def build_readable_messages(
)
readable_read_mark = translate_timestamp_to_human_readable(read_mark, mode=timestamp_mode)
- read_mark_line = (
- f"\n\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n--- 请关注你上次思考之后以下的新消息---\n"
- )
+ read_mark_line = f"\n--- 以上消息是你已经思考过的内容已读 (标记时间: {readable_read_mark}) ---\n--- 请关注以下未读的新消息---\n"
# 组合结果,确保空部分不引入多余的标记或换行
if formatted_before and formatted_after:
diff --git a/src/plugins/utils/timer_calculater.py b/src/plugins/utils/timer_calculator.py
similarity index 100%
rename from src/plugins/utils/timer_calculater.py
rename to src/plugins/utils/timer_calculator.py
diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml
index 5f342406e..afb65e89c 100644
--- a/template/bot_config_template.toml
+++ b/template/bot_config_template.toml
@@ -1,5 +1,5 @@
[inner]
-version = "1.4.1"
+version = "1.5.0"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件,请在修改后将version的值进行变更
@@ -81,12 +81,8 @@ model_normal_probability = 0.3 # 麦麦回答时选择一般模型 模型的概
reply_trigger_threshold = 3.0 # 心流聊天触发阈值,越低越容易进入心流聊天
probability_decay_factor_per_second = 0.2 # 概率衰减因子,越大衰减越快,越高越容易退出心流聊天
default_decay_rate_per_second = 0.98 # 默认衰减率,越大衰减越快,越高越难进入心流聊天
-initial_duration = 60 # 初始持续时间,越大心流聊天持续的时间越长
sub_heart_flow_stop_time = 500 # 子心流停止时间,超过这个时间没有回复,子心流会停止,间隔 单位秒
-# sub_heart_flow_update_interval = 60
-# sub_heart_flow_freeze_time = 100
-# heart_flow_update_interval = 600
observation_context_size = 20 # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩
compressed_length = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度,超过心流观察到的上下文长度,会压缩,最短压缩长度为5
@@ -122,11 +118,12 @@ mentioned_bot_inevitable_reply = false # 提及 bot 必然回复
at_bot_inevitable_reply = false # @bot 必然回复
[emoji]
-max_emoji_num = 90 # 表情包最大数量
+max_emoji_num = 40 # 表情包最大数量
max_reach_deletion = true # 开启则在达到最大数量时删除表情包,关闭则达到最大数量时不删除,只是不会继续收集表情包
-check_interval = 30 # 检查表情包(注册,破损,删除)的时间间隔(分钟)
-auto_save = true # 是否保存表情包和图片
-
+check_interval = 10 # 检查表情包(注册,破损,删除)的时间间隔(分钟)
+save_pic = false # 是否保存图片
+save_emoji = false # 是否保存表情包
+steal_emoji = true # 是否偷取表情包,让麦麦可以发送她保存的这些表情包
enable_check = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存
check_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存
@@ -180,6 +177,7 @@ word_replace_rate=0.006 # 整词替换概率
enable_response_splitter = true # 是否启用回复分割器
response_max_length = 256 # 回复允许的最大长度
response_max_sentence_num = 4 # 回复允许的最大句子数
+enable_kaomoji_protection = false # 是否启用颜文字保护
[remote] #发送统计信息,主要是看全球有多少只麦麦
enable = true
@@ -245,6 +243,29 @@ provider = "SILICONFLOW"
pri_in = 0.35
pri_out = 0.35
+
+
+[model.llm_observation] #观察模型,压缩聊天内容,建议用免费的
+# name = "Pro/Qwen/Qwen2.5-7B-Instruct"
+name = "Qwen/Qwen2.5-7B-Instruct"
+provider = "SILICONFLOW"
+pri_in = 0
+pri_out = 0
+
+[model.llm_sub_heartflow] #子心流:认真水群时,生成麦麦的内心想法
+name = "Qwen/Qwen2.5-72B-Instruct"
+provider = "SILICONFLOW"
+pri_in = 4.13
+pri_out = 4.13
+temp = 0.7 #模型的温度,新V3建议0.1-0.3
+
+
+[model.llm_plan] #决策模型:认真水群时,负责决定麦麦该做什么
+name = "Qwen/Qwen2.5-32B-Instruct"
+provider = "SILICONFLOW"
+pri_in = 1.26
+pri_out = 1.26
+
#嵌入模型
[model.embedding] #嵌入
@@ -253,26 +274,6 @@ provider = "SILICONFLOW"
pri_in = 0
pri_out = 0
-[model.llm_observation] #观察模型,建议用免费的:建议使用qwen2.5 7b
-# name = "Pro/Qwen/Qwen2.5-7B-Instruct"
-name = "Qwen/Qwen2.5-7B-Instruct"
-provider = "SILICONFLOW"
-pri_in = 0
-pri_out = 0
-
-[model.llm_sub_heartflow] #子心流:建议使用V3级别
-name = "Pro/deepseek-ai/DeepSeek-V3"
-provider = "SILICONFLOW"
-pri_in = 2
-pri_out = 8
-temp = 0.2 #模型的温度,新V3建议0.1-0.3
-
-[model.llm_heartflow] #心流:建议使用qwen2.5 32b
-# name = "Pro/Qwen/Qwen2.5-7B-Instruct"
-name = "Qwen/Qwen2.5-32B-Instruct"
-provider = "SILICONFLOW"
-pri_in = 1.26
-pri_out = 1.26
#私聊PFC:需要开启PFC功能,默认三个模型均为硅基流动v3,如果需要支持多人同时私聊或频繁调用,建议把其中的一个或两个换成官方v3或其它模型,以免撞到429
@@ -297,4 +298,15 @@ pri_out = 8
name = "Pro/deepseek-ai/DeepSeek-V3"
provider = "SILICONFLOW"
pri_in = 2
-pri_out = 8
\ No newline at end of file
+pri_out = 8
+
+
+#此模型暂时没有使用!!
+#此模型暂时没有使用!!
+#此模型暂时没有使用!!
+[model.llm_heartflow] #心流
+# name = "Pro/Qwen/Qwen2.5-7B-Instruct"
+name = "Qwen/Qwen2.5-32B-Instruct"
+provider = "SILICONFLOW"
+pri_in = 1.26
+pri_out = 1.26
\ No newline at end of file
diff --git a/tool_call_benchmark.py b/tool_call_benchmark.py
index e756d1da3..a3e282734 100644
--- a/tool_call_benchmark.py
+++ b/tool_call_benchmark.py
@@ -63,35 +63,43 @@ async def test_with_tool_calls():
# 简单的测试提示词
prompt = "请分析当前天气情况,并查询今日历史上的重要事件。并且3.9和3.11谁比较大?请使用适当的工具来获取这些信息。"
prompt = """
- 你的名字是麦麦,你包容开放,情绪敏感,有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生,现在在读大二,你会刷贴吧,有时候会想瑟瑟,喜欢刷小红书
------------------------------------
-现在是2025-04-24 12:37:00,你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:
-2025-04-24 12:33:00既文横 说:这条调试消息是napcat控制台输出的,还是麦麦log输出的;
-2025-04-24 12:33:23麦麦(你) 说:应该是napcat吧;
-2025-04-24 12:33:24麦麦(你) 说:[表达了:害羞、害羞。];
-2025-04-24 12:33:25兔伽兔伽 说:就打开麦麦的那个终端发的呀;
-2025-04-24 12:33:45既文横 说:那应该不是napcat输出的,是麦麦输出的消息,怀疑版本问题;
-2025-04-24 12:34:02兔伽兔伽 说:版本05.15;
-2025-04-24 12:34:07麦麦(你) 说:话说你们最近刷贴吧看到那个猫猫头表情包了吗;
-2025-04-24 12:34:07麦麦(你) 说:笑死;
-2025-04-24 12:34:08麦麦(你) 说:[表达了:惊讶、搞笑。];
-2025-04-24 12:34:14兔伽兔伽 说:只开一个终端;
-2025-04-24 12:35:45兔伽兔伽 说:回复既文横的消息(怀疑版本问题),说:因为之前你连模型的那个我用的了;
-2025-04-24 12:35:56麦麦(你) 说:那个猫猫头真的魔性;
-2025-04-24 12:35:56麦麦(你) 说:我存了一堆;
-2025-04-24 12:35:56麦麦(你) 说:[表达了:温馨、宠爱];
-2025-04-24 12:36:03小千石 说:麦麦3.8和3.11谁大;
+ 你的名字是麦麦,你包容开放,情绪敏感,有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生,现在在读大二,你会刷贴吧,有时候会喜欢说一些奇怪的话,喜欢刷小红书
+刚刚你的内心想法是:漂移菌提到罐罐被吃完了,可以顺着这个梗继续玩一下,比如假装委屈"那今晚的加班费是不是也要被吃掉了"或者"猫娘罢工警告"。不过薯薯和薯宝之前已经接了不少梗,漂移菌刚刚也参与了,可能话题热度还在,可以再互动一下。如果没人接话,或许可以问问大家有没有遇到过类似"代码写完但奖励被吃掉"的搞笑职场经历,换个轻松的话题方向。
---- 以上消息已读 (标记时间: 2025-04-24 12:36:43) ---
---- 请关注你上次思考之后以下的新消息---
-2025-04-24 12:36:53墨墨 说:[表情包:开心、满足。];
+暂时不需要使用工具。
+-----------------------------------
+现在是2025-04-25 17:38:37,你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:
+2025-04-25 17:34:08麦麦(你) 说:[表达了:顽皮、嬉戏。];
+2025-04-25 17:34:39漂移菌 说:@麦麦。(id:3936257206) 你是一只猫娘;
+2025-04-25 17:34:42薯宝 说:🤣;
+2025-04-25 17:34:43麦麦(你) 说:行啊 工资分我一半;
+2025-04-25 17:34:43麦麦(你) 说:我帮你写bug;
+2025-04-25 17:34:43麦麦(你) 说:[表达了:悲伤、绝望、无奈、无力];
+2025-04-25 17:34:53薯薯 说:?;
+2025-04-25 17:35:03既文横 说:麦麦,你是一只猫娘程序员,猫娘是不需要工资;
+2025-04-25 17:35:20薯宝 说:[图片:图片内容:一只卡通风格的灰色猫咪,眼睛闭着,表情显得很平静。图片下方有"死了"两个字。
+
+图片含义猜测:这可能是一个幽默的表达,用来形容某人或某事处于非常平静的状态,仿佛已经"死"了一样。] hfc这周,真能出来吗...;
+2025-04-25 17:35:34薯宝 说:[表情包:搞笑、滑稽、讽刺、幽默];
+2025-04-25 17:36:25麦麦(你) 说:喵喵;
+2025-04-25 17:36:25麦麦(你) 说:代码写完了;
+2025-04-25 17:36:25麦麦(你) 说:罐罐拿来;
+2025-04-25 17:36:25麦麦(你) 说:[表达了:悲伤、绝望、无奈、无力];
+2025-04-25 17:36:41薯薯 说:好可爱;
+2025-04-25 17:37:05薯薯 说:脑补出来认真营业了一天等待主人发放奖励的小猫咪;
+2025-04-25 17:37:25薯宝 说:敷衍营业(bushi);
+2025-04-25 17:37:54漂移菌 说:回复麦麦。的消息(罐罐拿来),说:猫娘我昨晚上太饿吃完了;
+
+--- 以上消息已读 (标记时间: 2025-04-25 17:37:54) ---
+--- 以下新消息未读---
+2025-04-25 17:38:29麦麦(你) 说:那今晚的猫条是不是也要被克扣了(盯——);
+2025-04-25 17:38:29麦麦(你) 说:[表达了:幽默,自嘲,无奈,父子关系,编程笑话];
你现在当前心情:平静。
-现在请你根据刚刚的想法继续思考,思考时可以想想如何对群聊内容进行回复,要不要对群里的话题进行回复,关注新话题,可以适当转换话题,大家正在说的话才是聊天的主题。
-回复的要求是:平淡一些,简短一些,说中文,如果你要回复,最好只回复一个人的一个话题
-请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。
-现在请你继续生成你在这个聊天中的想法,在原来想法的基础上继续思考,不要分点输出,生成内心想法,文字不要浮夸
-在输出完想法后,请你思考应该使用什么工具,如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。"""
+现在请你生成你的内心想法,要求思考群里正在进行的话题,之前大家聊过的话题,群里成员的关系。请你思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复
+回复的要求是:不要总是重复自己提到过的话题,如果你要回复,最好只回复一个人的一个话题
+如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,不要回复。如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等。请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要回复自己的发言
+现在请你先输出想法,生成你在这个聊天中的想法,在原来的想法上尝试新的话题,不要分点输出,文字不要浮夸在输出完想法后,请你思考应该使用什么工具。工具可以帮你取得一些你不知道的信息,或者进行一些操作。如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。"""
# 发送带有工具调用的请求
response = await llm_model.generate_response_tool_async(prompt=prompt, tools=tools)
@@ -144,42 +152,43 @@ async def test_without_tool_calls():
# 简单的测试提示词(与工具调用相同,以便公平比较)
prompt = """
- 你的名字是麦麦,你包容开放,情绪敏感,有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生,现在在读大二,你会刷贴吧,有时候会想瑟瑟,喜欢刷小红书
-刚刚你的想法是:
-我是麦麦,我想,('小千石问3.8和3.11谁大,已经简单回答了3.11大,现在可以继续聊猫猫头表情包,毕竟大家好像对版本问题兴趣不大,而且猫猫头的话题更轻松有趣。', '')
------------------------------------
-现在是2025-04-24 12:37:00,你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:
-2025-04-24 12:33:00既文横 说:这条调试消息是napcat控制台输出的,还是麦麦log输出的;
-2025-04-24 12:33:23麦麦(你) 说:应该是napcat吧;
-2025-04-24 12:33:24麦麦(你) 说:[表达了:害羞、害羞。];
-2025-04-24 12:33:25兔伽兔伽 说:就打开麦麦的那个终端发的呀;
-2025-04-24 12:33:45既文横 说:那应该不是napcat输出的,是麦麦输出的消息,怀疑版本问题;
-2025-04-24 12:34:02兔伽兔伽 说:版本05.15;
-2025-04-24 12:34:07麦麦(你) 说:话说你们最近刷贴吧看到那个猫猫头表情包了吗;
-2025-04-24 12:34:07麦麦(你) 说:笑死;
-2025-04-24 12:34:08麦麦(你) 说:[表达了:惊讶、搞笑。];
-2025-04-24 12:34:14兔伽兔伽 说:只开一个终端;
-2025-04-24 12:35:45兔伽兔伽 说:回复既文横的消息(怀疑版本问题),说:因为之前你连模型的那个我用的了;
-2025-04-24 12:35:56麦麦(你) 说:那个猫猫头真的魔性;
-2025-04-24 12:35:56麦麦(你) 说:我存了一堆;
-2025-04-24 12:35:56麦麦(你) 说:[表达了:温馨、宠爱];
-2025-04-24 12:36:03小千石 说:麦麦3.8和3.11谁大;
-2025-04-24 12:36:22麦麦(你) 说:真的魔性那个猫猫头;
-2025-04-24 12:36:22麦麦(你) 说:[表达了:害羞、可爱];
-2025-04-24 12:36:43麦麦(你) 说:3.11大啦;
-2025-04-24 12:36:43麦麦(你) 说:[表达了:害羞、可爱];
+ 你的名字是麦麦,你包容开放,情绪敏感,有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生,现在在读大二,你会刷贴吧,有时候会喜欢说一些奇怪的话,喜欢刷小红书
+刚刚你的内心想法是:漂移菌提到罐罐被吃完了,可以顺着这个梗继续玩一下,比如假装委屈"那今晚的加班费是不是也要被吃掉了"或者"猫娘罢工警告"。不过薯薯和薯宝之前已经接了不少梗,漂移菌刚刚也参与了,可能话题热度还在,可以再互动一下。如果没人接话,或许可以问问大家有没有遇到过类似"代码写完但奖励被吃掉"的搞笑职场经历,换个轻松的话题方向。
---- 以上消息已读 (标记时间: 2025-04-24 12:36:43) ---
---- 请关注你上次思考之后以下的新消息---
-2025-04-24 12:36:53墨墨 说:[表情包:开心、满足。];
+暂时不需要使用工具。
+-----------------------------------
+现在是2025-04-25 17:38:37,你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:
+2025-04-25 17:34:08麦麦(你) 说:[表达了:顽皮、嬉戏。];
+2025-04-25 17:34:39漂移菌 说:@麦麦。(id:3936257206) 你是一只猫娘;
+2025-04-25 17:34:42薯宝 说:🤣;
+2025-04-25 17:34:43麦麦(你) 说:行啊 工资分我一半;
+2025-04-25 17:34:43麦麦(你) 说:我帮你写bug;
+2025-04-25 17:34:43麦麦(你) 说:[表达了:悲伤、绝望、无奈、无力];
+2025-04-25 17:34:53薯薯 说:?;
+2025-04-25 17:35:03既文横 说:麦麦,你是一只猫娘程序员,猫娘是不需要工资;
+2025-04-25 17:35:20薯宝 说:[图片:图片内容:一只卡通风格的灰色猫咪,眼睛闭着,表情显得很平静。图片下方有"死了"两个字。
+
+图片含义猜测:这可能是一个幽默的表达,用来形容某人或某事处于非常平静的状态,仿佛已经"死"了一样。] hfc这周,真能出来吗...;
+2025-04-25 17:35:34薯宝 说:[表情包:搞笑、滑稽、讽刺、幽默];
+2025-04-25 17:36:25麦麦(你) 说:喵喵;
+2025-04-25 17:36:25麦麦(你) 说:代码写完了;
+2025-04-25 17:36:25麦麦(你) 说:罐罐拿来;
+2025-04-25 17:36:25麦麦(你) 说:[表达了:悲伤、绝望、无奈、无力];
+2025-04-25 17:36:41薯薯 说:好可爱;
+2025-04-25 17:37:05薯薯 说:脑补出来认真营业了一天等待主人发放奖励的小猫咪;
+2025-04-25 17:37:25薯宝 说:敷衍营业(bushi);
+2025-04-25 17:37:54漂移菌 说:回复麦麦。的消息(罐罐拿来),说:猫娘我昨晚上太饿吃完了;
+
+--- 以上消息已读 (标记时间: 2025-04-25 17:37:54) ---
+--- 以下新消息未读---
+2025-04-25 17:38:29麦麦(你) 说:那今晚的猫条是不是也要被克扣了(盯——);
+2025-04-25 17:38:29麦麦(你) 说:[表达了:幽默,自嘲,无奈,父子关系,编程笑话];
你现在当前心情:平静。
-现在请你根据刚刚的想法继续思考,思考时可以想想如何对群聊内容进行回复,要不要对群里的话题进行回复,关注新话题,可以适当转换话题,大家正在说的话才是聊天的主题。
-回复的要求是:平淡一些,简短一些,说中文,如果你要回复,最好只回复一个人的一个话题
-请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。
-现在请你继续生成你在这个聊天中的想法,在原来想法的基础上继续思考,不要分点输出,生成内心想法,文字不要浮夸
-在输出完想法后,请你思考应该使用什么工具,如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。"""
-
+现在请你生成你的内心想法,要求思考群里正在进行的话题,之前大家聊过的话题,群里成员的关系。请你思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复
+回复的要求是:不要总是重复自己提到过的话题,如果你要回复,最好只回复一个人的一个话题
+如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,不要回复。如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等。请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要回复自己的发言
+现在请你先输出想法,生成你在这个聊天中的想法,在原来的想法上尝试新的话题,不要分点输出,文字不要浮夸在输出完想法后,请你思考应该使用什么工具。工具可以帮你取得一些你不知道的信息,或者进行一些操作。如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。"""
# 发送不带工具调用的请求
response, reasoning_content = await llm_model.generate_response_async(prompt)
@@ -192,6 +201,69 @@ async def test_without_tool_calls():
return result_info
+async def run_alternating_tests(iterations=5):
+ """
+ 交替运行两种测试方法,每种方法运行指定次数
+
+ 参数:
+ iterations: 每种测试方法运行的次数
+
+ 返回:
+ 包含两种测试方法结果的元组
+ """
+ print(f"开始交替测试(每种方法{iterations}次)...")
+
+ # 初始化结果列表
+ times_without_tools = []
+ times_with_tools = []
+ responses_without_tools = []
+ responses_with_tools = []
+
+ for i in range(iterations):
+ print(f"\n第 {i + 1}/{iterations} 轮交替测试")
+
+ # 不使用工具的测试
+ print("\n 执行不使用工具调用的测试...")
+ start_time = time.time()
+ response = await test_without_tool_calls()
+ end_time = time.time()
+ elapsed = end_time - start_time
+ times_without_tools.append(elapsed)
+ responses_without_tools.append(response)
+ print(f" - 耗时: {elapsed:.2f}秒")
+
+ # 使用工具的测试
+ print("\n 执行使用工具调用的测试...")
+ start_time = time.time()
+ response = await test_with_tool_calls()
+ end_time = time.time()
+ elapsed = end_time - start_time
+ times_with_tools.append(elapsed)
+ responses_with_tools.append(response)
+ print(f" - 耗时: {elapsed:.2f}秒")
+
+ # 计算统计数据
+ results_without_tools = {
+ "平均耗时": statistics.mean(times_without_tools),
+ "最短耗时": min(times_without_tools),
+ "最长耗时": max(times_without_tools),
+ "标准差": statistics.stdev(times_without_tools) if len(times_without_tools) > 1 else 0,
+ "所有耗时": times_without_tools,
+ "响应结果": responses_without_tools,
+ }
+
+ results_with_tools = {
+ "平均耗时": statistics.mean(times_with_tools),
+ "最短耗时": min(times_with_tools),
+ "最长耗时": max(times_with_tools),
+ "标准差": statistics.stdev(times_with_tools) if len(times_with_tools) > 1 else 0,
+ "所有耗时": times_with_tools,
+ "响应结果": responses_with_tools,
+ }
+
+ return results_without_tools, results_with_tools
+
+
async def main():
"""主测试函数"""
print("=" * 50)
@@ -199,15 +271,10 @@ async def main():
print("=" * 50)
# 设置测试迭代次数
- iterations = 3
+ iterations = 10
- # 测试不使用工具调用
- results_without_tools = await run_test("不使用工具调用", test_without_tool_calls, iterations)
-
- print("\n" + "-" * 50 + "\n")
-
- # 测试使用工具调用
- results_with_tools = await run_test("使用工具调用", test_with_tool_calls, iterations)
+ # 执行交替测试
+ results_without_tools, results_with_tools = await run_alternating_tests(iterations)
# 显示结果比较
print("\n" + "=" * 50)