diff --git a/README.md b/README.md
index 820d7d623..58cb82c7d 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
### 📢 版本信息
-**最新版本: v0.6.2** ([查看更新日志](changelogs/changelog.md))
+**最新版本: v0.6.3** ([查看更新日志](changelogs/changelog.md))
> [!WARNING]
> 请阅读教程后更新!!!!!!!
> 请阅读教程后更新!!!!!!!
@@ -110,19 +110,20 @@
- [🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy_windows.html) - 基于MaiCore的新版本部署方式(与旧版本不兼容)
-## 🎯 功能介绍
+## 🎯 0.6.3 功能介绍
| 模块 | 主要功能 | 特点 |
|----------|------------------------------------------------------------------|-------|
-| 💬 聊天系统 | • 心流/推理聊天
• 关键词主动发言
• 多模型支持
• 动态prompt构建
• 私聊功能(PFC) | 拟人化交互 |
-| 🧠 心流系统 | • 实时思考生成
• 自动启停机制
• 日程系统联动
• 工具调用能力 | 智能化决策 |
-| 🧠 记忆系统 | • 优化记忆抽取
• 海马体记忆机制
• 聊天记录概括 | 持久化记忆 |
-| 😊 表情系统 | • 情绪匹配发送
• GIF支持
• 自动收集与审查 | 丰富表达 |
+| 💬 聊天系统 | • **统一调控不同回复逻辑**
• 智能交互模式 (普通聊天/专注聊天)
• 关键词主动发言
• 多模型支持
• 动态prompt构建
• 私聊功能(PFC)增强 | 拟人化交互 |
+| 🧠 心流系统 | • 实时思考生成
• **智能状态管理**
• **概率回复机制**
• 自动启停机制
• 日程系统联动
• **上下文感知工具调用** | 智能化决策 |
+| 🧠 记忆系统 | • **记忆整合与提取**
• 海马体记忆机制
• 聊天记录概括 | 持久化记忆 |
+| 😊 表情系统 | • **全新表情包系统**
• **优化选择逻辑**
• 情绪匹配发送
• GIF支持
• 自动收集与审查 | 丰富表达 |
| 📅 日程系统 | • 动态日程生成
• 自定义想象力
• 思维流联动 | 智能规划 |
-| 👥 关系系统 | • 关系管理优化
• 丰富接口支持
• 个性化交互 | 深度社交 |
+| 👥 关系系统 | • **工具调用动态更新**
• 关系管理优化
• 丰富接口支持
• 个性化交互 | 深度社交 |
| 📊 统计系统 | • 使用数据统计
• LLM调用记录
• 实时控制台显示 | 数据可视 |
-| 🔧 系统功能 | • 优雅关闭机制
• 自动数据保存
• 异常处理完善 | 稳定可靠 |
-| 🛠️ 工具系统 | • 知识获取工具
• 自动注册机制
• 多工具支持 | 扩展功能 |
+| 🛠️ 工具系统 | • **LPMM知识库集成**
• **上下文感知调用**
• 知识获取工具
• 自动注册机制
• 多工具支持 | 扩展功能 |
+| 📚 **知识库(LPMM)** | • **全新LPMM系统**
• **强大的信息检索能力** | 知识增强 |
+| ✨ **昵称系统** | • **自动为群友取昵称**
• **降低认错人概率** (早期阶段) | 身份识别 |
## 📐 项目架构
@@ -142,18 +143,6 @@ graph TD
E --> M[情绪识别]
```
-
-## 开发计划TODO:LIST
-
-- 人格功能:WIP
-- 对特定对象的侧写功能
-- 图片发送,转发功能:WIP
-- 幽默和meme功能:WIP
-- 兼容gif的解析和保存
-- 小程序转发链接解析
-- 修复已知bug
-- 自动生成的回复逻辑,例如自生成的回复方向,回复风格
-
## ✍️如何给本项目报告BUG/提交建议/做贡献
MaiCore是一个开源项目,我们非常欢迎你的参与。你的贡献,无论是提交bug报告、功能需求还是代码pr,都对项目非常宝贵。我们非常感谢你的支持!🎉 但无序的讨论会降低沟通效率,进而影响问题的解决速度,因此在提交任何贡献前,请务必先阅读本项目的[贡献指南](depends-data/CONTRIBUTE.md)(待补完)
diff --git a/changelogs/changelog_config.md b/changelogs/changelog_config.md
index e438ea31e..5aa5fb922 100644
--- a/changelogs/changelog_config.md
+++ b/changelogs/changelog_config.md
@@ -33,7 +33,7 @@
- 调整了部分配置项的默认值
- 调整了配置项的顺序,将 `groups` 配置项移到了更靠前的位置
- 在 `message` 配置项中:
- - 新增了 `max_response_length` 参数
+ - 新增了 `model_max_output_length` 参数
- 在 `willing` 配置项中新增了 `emoji_response_penalty` 参数
- 将 `personality` 配置项中的 `prompt_schedule` 重命名为 `prompt_schedule_gen`
diff --git a/scripts/interest_monitor_gui.py b/scripts/interest_monitor_gui.py
index adb83f729..1f03b9695 100644
--- a/scripts/interest_monitor_gui.py
+++ b/scripts/interest_monitor_gui.py
@@ -344,9 +344,6 @@ class InterestMonitorApp:
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"
- ) # 存储原始时间戳
# 添加数据点 (使用顶层时间戳)
new_stream_history[stream_id].append((entry_timestamp, interest_level_float))
diff --git a/src/api/config_api.py b/src/api/config_api.py
index e39346176..025888d82 100644
--- a/src/api/config_api.py
+++ b/src/api/config_api.py
@@ -47,7 +47,7 @@ class BotConfig:
MAX_CONTEXT_SIZE: int # 上下文最大消息数
emoji_chance: float # 发送表情包的基础概率
thinking_timeout: int # 思考时间
- max_response_length: int # 最大回复长度
+ model_max_output_length: int # 最大回复长度
message_buffer: bool # 消息缓冲器
ban_words: set
@@ -132,7 +132,7 @@ class BotConfig:
# llm_reasoning_minor: Dict[str, str]
llm_normal: Dict[str, str] # LLM普通
llm_topic_judge: Dict[str, str] # LLM话题判断
- llm_summary_by_topic: Dict[str, str] # LLM话题总结
+ llm_summary: Dict[str, str] # LLM话题总结
llm_emotion_judge: Dict[str, str] # LLM情感判断
embedding: Dict[str, str] # 嵌入
vlm: Dict[str, str] # VLM
diff --git a/src/common/logger.py b/src/common/logger.py
index 43972e1f2..6c95935ea 100644
--- a/src/common/logger.py
+++ b/src/common/logger.py
@@ -621,25 +621,24 @@ CHAT_IMAGE_STYLE_CONFIG = {
},
}
-# 兴趣log
-INTEREST_STYLE_CONFIG = {
+# HFC log
+HFC_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}"
- ),
- "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 兴趣 | {message}",
+ "console_format": ("{time:MM-DD HH:mm} | 专注聊天 | {message}"),
+ "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
},
}
+
CONFIRM_STYLE_CONFIG = {
"console_format": "{message}", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}",
@@ -847,7 +846,7 @@ CONFIG_STYLE_CONFIG = CONFIG_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CONFIG
TOOL_USE_STYLE_CONFIG = TOOL_USE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TOOL_USE_STYLE_CONFIG["advanced"]
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"]
+HFC_STYLE_CONFIG = HFC_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else HFC_STYLE_CONFIG["advanced"]
TIANYI_STYLE_CONFIG = TIANYI_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TIANYI_STYLE_CONFIG["advanced"]
MODEL_UTILS_STYLE_CONFIG = MODEL_UTILS_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MODEL_UTILS_STYLE_CONFIG["advanced"]
PROMPT_STYLE_CONFIG = PROMPT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PROMPT_STYLE_CONFIG["advanced"]
diff --git a/src/common/logger_manager.py b/src/common/logger_manager.py
index ab1861e2b..5c5538385 100644
--- a/src/common/logger_manager.py
+++ b/src/common/logger_manager.py
@@ -23,7 +23,7 @@ from src.common.logger import (
PFC_ACTION_PLANNER_STYLE_CONFIG,
MAI_STATE_CONFIG,
LPMM_STYLE_CONFIG,
- INTEREST_STYLE_CONFIG,
+ HFC_STYLE_CONFIG,
TIANYI_STYLE_CONFIG,
REMOTE_STYLE_CONFIG,
TOPIC_STYLE_CONFIG,
@@ -68,7 +68,7 @@ MODULE_LOGGER_CONFIGS = {
"pfc_action_planner": PFC_ACTION_PLANNER_STYLE_CONFIG, # PFC私聊规划
"mai_state": MAI_STATE_CONFIG, # 麦麦状态
"lpmm": LPMM_STYLE_CONFIG, # LPMM
- "interest": INTEREST_STYLE_CONFIG, # 兴趣
+ "hfc": HFC_STYLE_CONFIG, # HFC
"tianyi": TIANYI_STYLE_CONFIG, # 天依
"remote": REMOTE_STYLE_CONFIG, # 远程
"topic": TOPIC_STYLE_CONFIG, # 话题
diff --git a/src/config/config.py b/src/config/config.py
index f09da9a7e..fbf558a37 100644
--- a/src/config/config.py
+++ b/src/config/config.py
@@ -20,9 +20,9 @@ from src.common.logger_manager import get_logger
logger = get_logger("config")
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
-is_test = True
+is_test = False
mai_version_main = "0.6.3"
-mai_version_fix = "snapshot-5"
+mai_version_fix = "fix-1"
if mai_version_fix:
if is_test:
@@ -170,32 +170,34 @@ class BotConfig:
SCHEDULE_TEMPERATURE: float = 0.5 # 日程表温度,建议0.5-1.0
TIME_ZONE: str = "Asia/Shanghai" # 时区
- # message
- MAX_CONTEXT_SIZE: int = 15 # 上下文最大消息数
- emoji_chance: float = 0.2 # 发送表情包的基础概率
- thinking_timeout: int = 120 # 思考时间
- max_response_length: int = 1024 # 最大回复长度
+ # chat
+ allow_focus_mode: bool = True # 是否允许专注聊天状态
+
+ base_normal_chat_num: int = 3 # 最多允许多少个群进行普通聊天
+ base_focused_chat_num: int = 2 # 最多允许多少个群进行专注聊天
+
+ observation_context_size: int = 12 # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩
+
message_buffer: bool = True # 消息缓冲器
ban_words = set()
ban_msgs_regex = set()
- # [heartflow] # 启用启用heart_flowC(心流聊天)模式时生效, 需要填写token消耗量巨大的相关模型
- # 启用后麦麦会自主选择进入heart_flowC模式(持续一段时间), 进行长时间高质量的聊天
+ # focus_chat
reply_trigger_threshold: float = 3.0 # 心流聊天触发阈值,越低越容易触发
- probability_decay_factor_per_second: float = 0.2 # 概率衰减因子,越大衰减越快
default_decay_rate_per_second: float = 0.98 # 默认衰减率,越大衰减越慢
- allow_focus_mode: bool = True # 是否允许子心流进入 FOCUSED 状态
+ consecutive_no_reply_threshold = 3
- # sub_heart_flow_update_interval: int = 60 # 子心流更新频率,间隔 单位秒
- # sub_heart_flow_freeze_time: int = 120 # 子心流冻结时间,超过这个时间没有回复,子心流会冻结,间隔 单位秒
- sub_heart_flow_stop_time: int = 600 # 子心流停止时间,超过这个时间没有回复,子心流会停止,间隔 单位秒
- # heart_flow_update_interval: int = 300 # 心流更新频率,间隔 单位秒
- observation_context_size: int = 20 # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩
compressed_length: int = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度,超过心流观察到的上下文长度,会压缩,最短压缩长度为5
compress_length_limit: int = 5 # 最多压缩份数,超过该数值的压缩上下文会被删除
- # willing
+ # normal_chat
+ model_reasoning_probability: float = 0.7 # 麦麦回答时选择推理模型(主要)模型概率
+ model_normal_probability: float = 0.3 # 麦麦回答时选择一般模型(次要)模型概率
+
+ emoji_chance: float = 0.2 # 发送表情包的基础概率
+ thinking_timeout: int = 120 # 思考时间
+
willing_mode: str = "classical" # 意愿模式
response_willing_amplifier: float = 1.0 # 回复意愿放大系数
response_interested_rate_amplifier: float = 1.0 # 回复兴趣度放大系数
@@ -204,12 +206,6 @@ class BotConfig:
mentioned_bot_inevitable_reply: bool = False # 提及 bot 必然回复
at_bot_inevitable_reply: bool = False # @bot 必然回复
- # response
- response_mode: str = "heart_flow" # 回复策略
- model_reasoning_probability: float = 0.7 # 麦麦回答时选择推理模型(主要)模型概率
- model_normal_probability: float = 0.3 # 麦麦回答时选择一般模型(次要)模型概率
- # MODEL_R1_DISTILL_PROBABILITY: float = 0.1 # R1蒸馏模型概率
-
# emoji
max_emoji_num: int = 200 # 表情包最大数量
max_reach_deletion: bool = True # 开启则在达到最大数量时删除表情包,关闭则不会继续收集表情包
@@ -264,6 +260,8 @@ class BotConfig:
response_max_length = 100 # 回复允许的最大长度
response_max_sentence_num = 3 # 回复允许的最大句子数
+ model_max_output_length: int = 800 # 最大回复长度
+
# remote
remote_enable: bool = True # 是否启用远程控制
@@ -277,8 +275,7 @@ class BotConfig:
# llm_reasoning_minor: Dict[str, str] = field(default_factory=lambda: {})
llm_normal: Dict[str, str] = field(default_factory=lambda: {})
llm_topic_judge: Dict[str, str] = field(default_factory=lambda: {})
- llm_summary_by_topic: Dict[str, str] = field(default_factory=lambda: {})
- llm_emotion_judge: Dict[str, str] = field(default_factory=lambda: {})
+ llm_summary: Dict[str, str] = field(default_factory=lambda: {})
embedding: Dict[str, str] = field(default_factory=lambda: {})
vlm: Dict[str, str] = field(default_factory=lambda: {})
moderation: Dict[str, str] = field(default_factory=lambda: {})
@@ -409,63 +406,62 @@ class BotConfig:
config.BOT_NICKNAME = bot_config.get("nickname", config.BOT_NICKNAME)
config.BOT_ALIAS_NAMES = bot_config.get("alias_names", config.BOT_ALIAS_NAMES)
- def response(parent: dict):
- response_config = parent["response"]
- config.model_reasoning_probability = response_config.get(
+ def chat(parent: dict):
+ chat_config = parent["chat"]
+ config.allow_focus_mode = chat_config.get("allow_focus_mode", config.allow_focus_mode)
+ config.base_normal_chat_num = chat_config.get("base_normal_chat_num", config.base_normal_chat_num)
+ config.base_focused_chat_num = chat_config.get("base_focused_chat_num", config.base_focused_chat_num)
+ config.observation_context_size = chat_config.get(
+ "observation_context_size", config.observation_context_size
+ )
+ config.message_buffer = chat_config.get("message_buffer", config.message_buffer)
+ config.ban_words = chat_config.get("ban_words", config.ban_words)
+ for r in chat_config.get("ban_msgs_regex", config.ban_msgs_regex):
+ config.ban_msgs_regex.add(re.compile(r))
+
+ def normal_chat(parent: dict):
+ normal_chat_config = parent["normal_chat"]
+ config.model_reasoning_probability = normal_chat_config.get(
"model_reasoning_probability", config.model_reasoning_probability
)
- config.model_normal_probability = response_config.get(
+ config.model_normal_probability = normal_chat_config.get(
"model_normal_probability", config.model_normal_probability
)
+ config.emoji_chance = normal_chat_config.get("emoji_chance", config.emoji_chance)
+ config.thinking_timeout = normal_chat_config.get("thinking_timeout", config.thinking_timeout)
- def heartflow(parent: dict):
- heartflow_config = parent["heartflow"]
- config.sub_heart_flow_stop_time = heartflow_config.get(
- "sub_heart_flow_stop_time", config.sub_heart_flow_stop_time
+ config.willing_mode = normal_chat_config.get("willing_mode", config.willing_mode)
+ config.response_willing_amplifier = normal_chat_config.get(
+ "response_willing_amplifier", config.response_willing_amplifier
+ )
+ config.response_interested_rate_amplifier = normal_chat_config.get(
+ "response_interested_rate_amplifier", config.response_interested_rate_amplifier
+ )
+ config.down_frequency_rate = normal_chat_config.get("down_frequency_rate", config.down_frequency_rate)
+ config.emoji_response_penalty = normal_chat_config.get(
+ "emoji_response_penalty", config.emoji_response_penalty
)
- if config.INNER_VERSION in SpecifierSet(">=1.3.0"):
- config.observation_context_size = heartflow_config.get(
- "observation_context_size", config.observation_context_size
- )
- config.compressed_length = heartflow_config.get("compressed_length", config.compressed_length)
- config.compress_length_limit = heartflow_config.get(
- "compress_length_limit", config.compress_length_limit
- )
- if config.INNER_VERSION in SpecifierSet(">=1.4.0"):
- config.reply_trigger_threshold = heartflow_config.get(
- "reply_trigger_threshold", config.reply_trigger_threshold
- )
- config.probability_decay_factor_per_second = heartflow_config.get(
- "probability_decay_factor_per_second", config.probability_decay_factor_per_second
- )
- config.default_decay_rate_per_second = heartflow_config.get(
- "default_decay_rate_per_second", config.default_decay_rate_per_second
- )
- if config.INNER_VERSION in SpecifierSet(">=1.5.1"):
- config.allow_focus_mode = heartflow_config.get("allow_focus_mode", config.allow_focus_mode)
- def willing(parent: dict):
- willing_config = parent["willing"]
- config.willing_mode = willing_config.get("willing_mode", config.willing_mode)
+ config.mentioned_bot_inevitable_reply = normal_chat_config.get(
+ "mentioned_bot_inevitable_reply", config.mentioned_bot_inevitable_reply
+ )
+ config.at_bot_inevitable_reply = normal_chat_config.get(
+ "at_bot_inevitable_reply", config.at_bot_inevitable_reply
+ )
- if config.INNER_VERSION in SpecifierSet(">=0.0.11"):
- config.response_willing_amplifier = willing_config.get(
- "response_willing_amplifier", config.response_willing_amplifier
- )
- config.response_interested_rate_amplifier = willing_config.get(
- "response_interested_rate_amplifier", config.response_interested_rate_amplifier
- )
- config.down_frequency_rate = willing_config.get("down_frequency_rate", config.down_frequency_rate)
- config.emoji_response_penalty = willing_config.get(
- "emoji_response_penalty", config.emoji_response_penalty
- )
- if config.INNER_VERSION in SpecifierSet(">=1.2.5"):
- config.mentioned_bot_inevitable_reply = willing_config.get(
- "mentioned_bot_inevitable_reply", config.mentioned_bot_inevitable_reply
- )
- config.at_bot_inevitable_reply = willing_config.get(
- "at_bot_inevitable_reply", config.at_bot_inevitable_reply
- )
+ def focus_chat(parent: dict):
+ focus_chat_config = parent["focus_chat"]
+ config.compressed_length = focus_chat_config.get("compressed_length", config.compressed_length)
+ config.compress_length_limit = focus_chat_config.get("compress_length_limit", config.compress_length_limit)
+ config.reply_trigger_threshold = focus_chat_config.get(
+ "reply_trigger_threshold", config.reply_trigger_threshold
+ )
+ config.default_decay_rate_per_second = focus_chat_config.get(
+ "default_decay_rate_per_second", config.default_decay_rate_per_second
+ )
+ config.consecutive_no_reply_threshold = focus_chat_config.get(
+ "consecutive_no_reply_threshold", config.consecutive_no_reply_threshold
+ )
def model(parent: dict):
# 加载模型配置
@@ -476,8 +472,7 @@ class BotConfig:
# "llm_reasoning_minor",
"llm_normal",
"llm_topic_judge",
- "llm_summary_by_topic",
- "llm_emotion_judge",
+ "llm_summary",
"vlm",
"embedding",
"llm_tool_use",
@@ -556,26 +551,6 @@ class BotConfig:
logger.error(f"模型 {item} 在config中不存在,请检查,或尝试更新配置文件")
raise KeyError(f"模型 {item} 在config中不存在,请检查,或尝试更新配置文件")
- def message(parent: dict):
- msg_config = parent["message"]
- config.MAX_CONTEXT_SIZE = msg_config.get("max_context_size", config.MAX_CONTEXT_SIZE)
- config.emoji_chance = msg_config.get("emoji_chance", config.emoji_chance)
- config.ban_words = msg_config.get("ban_words", config.ban_words)
- config.thinking_timeout = msg_config.get("thinking_timeout", config.thinking_timeout)
- config.response_willing_amplifier = msg_config.get(
- "response_willing_amplifier", config.response_willing_amplifier
- )
- config.response_interested_rate_amplifier = msg_config.get(
- "response_interested_rate_amplifier", config.response_interested_rate_amplifier
- )
- config.down_frequency_rate = msg_config.get("down_frequency_rate", config.down_frequency_rate)
- for r in msg_config.get("ban_msgs_regex", config.ban_msgs_regex):
- config.ban_msgs_regex.add(re.compile(r))
- if config.INNER_VERSION in SpecifierSet(">=0.0.11"):
- config.max_response_length = msg_config.get("max_response_length", config.max_response_length)
- if config.INNER_VERSION in SpecifierSet(">=1.1.4"):
- config.message_buffer = msg_config.get("message_buffer", config.message_buffer)
-
def memory(parent: dict):
memory_config = parent["memory"]
config.build_memory_interval = memory_config.get("build_memory_interval", config.build_memory_interval)
@@ -650,6 +625,10 @@ class BotConfig:
config.enable_kaomoji_protection = response_splitter_config.get(
"enable_kaomoji_protection", config.enable_kaomoji_protection
)
+ if config.INNER_VERSION in SpecifierSet(">=1.6.0"):
+ config.model_max_output_length = response_splitter_config.get(
+ "model_max_output_length", config.model_max_output_length
+ )
def groups(parent: dict):
groups_config = parent["groups"]
@@ -695,10 +674,7 @@ class BotConfig:
"personality": {"func": personality, "support": ">=0.0.0"},
"identity": {"func": identity, "support": ">=1.2.4"},
"schedule": {"func": schedule, "support": ">=0.0.11", "necessary": False},
- "message": {"func": message, "support": ">=0.0.0"},
- "willing": {"func": willing, "support": ">=0.0.9", "necessary": False},
"emoji": {"func": emoji, "support": ">=0.0.0"},
- "response": {"func": response, "support": ">=0.0.0"},
"model": {"func": model, "support": ">=0.0.0"},
"memory": {"func": memory, "support": ">=0.0.0", "necessary": False},
"mood": {"func": mood, "support": ">=0.0.0"},
@@ -708,7 +684,9 @@ class BotConfig:
"platforms": {"func": platforms, "support": ">=1.0.0"},
"response_splitter": {"func": response_splitter, "support": ">=0.0.11", "necessary": False},
"experimental": {"func": experimental, "support": ">=0.0.11", "necessary": False},
- "heartflow": {"func": heartflow, "support": ">=1.0.2", "necessary": False},
+ "chat": {"func": chat, "support": ">=1.6.0", "necessary": False},
+ "normal_chat": {"func": normal_chat, "support": ">=1.6.0", "necessary": False},
+ "focus_chat": {"func": focus_chat, "support": ">=1.6.0", "necessary": False},
}
# 原地修改,将 字符串版本表达式 转换成 版本对象
diff --git a/src/do_tool/tool_can_use/base_tool.py b/src/do_tool/tool_can_use/base_tool.py
index 47f4a54dc..1dd15bafc 100644
--- a/src/do_tool/tool_can_use/base_tool.py
+++ b/src/do_tool/tool_can_use/base_tool.py
@@ -62,7 +62,7 @@ def register_tool(tool_class: Type[BaseTool]):
raise ValueError(f"工具类 {tool_class.__name__} 没有定义 name 属性")
TOOL_REGISTRY[tool_name] = tool_class
- logger.info(f"已注册工具: {tool_name}")
+ logger.info(f"已注册: {tool_name}")
def discover_tools():
diff --git a/src/do_tool/tool_can_use/lpmm_get_knowledge.py b/src/do_tool/tool_can_use/lpmm_get_knowledge.py
index 7aab84958..4dba1bc71 100644
--- a/src/do_tool/tool_can_use/lpmm_get_knowledge.py
+++ b/src/do_tool/tool_can_use/lpmm_get_knowledge.py
@@ -14,7 +14,7 @@ class SearchKnowledgeFromLPMMTool(BaseTool):
"""从LPMM知识库中搜索相关信息的工具"""
name = "lpmm_search_knowledge"
- description = "从知识库中搜索相关信息"
+ description = "从知识库中搜索相关信息,如果你需要知识,就使用这个工具"
parameters = {
"type": "object",
"properties": {
diff --git a/src/do_tool/tool_use.py b/src/do_tool/tool_use.py
index 5b886cb45..88289fe01 100644
--- a/src/do_tool/tool_use.py
+++ b/src/do_tool/tool_use.py
@@ -129,7 +129,6 @@ class ToolUser:
payload = {
"model": self.llm_model_tool.model_name,
"messages": [{"role": "user", "content": prompt}],
- "max_tokens": global_config.max_response_length,
"tools": tools,
"temperature": 0.2,
}
diff --git a/src/heart_flow/0.6.3TODO.md b/src/heart_flow/0.6.3TODO.md
deleted file mode 100644
index 5e9e96ab9..000000000
--- a/src/heart_flow/0.6.3TODO.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# 0.6.3 版本发布前待办事项
-
-- [0.6.3]**统一化人格配置:**
- - 检查代码中是否存在硬编码的人格相关配置。
- - 将所有硬编码的人格配置替换为使用 `individual` 模块进行管理。
-
-- [0.6.3]**在 Planner 中添加回复计数信息:**
- - 修改 `HeartFlowChatInstance` 的 `Plan` 阶段逻辑。
- - 将当前周期的回复计数(或其他相关统计信息)作为输入提供给 Planner。
- - 目的是为 Planner 提供负反馈,减少连续回复或不当回复的可能性。
-
-- [0.6.3]**恢复/检查被停止的功能:**
- - 全面审查代码,特别是对比之前的版本或设计文档。
- - 识别并重新启用那些暂时被禁用但应该恢复的功能。
- - 确认没有核心功能意外丢失。
-
-- [0.6.3]**参数提取与配置化:**
- - 识别代码中散落的各种可调参数(例如:概率阈值、时间间隔、次数限制、LLM 模型名称等)。
- - 将这些参数统一提取到模块或类的顶部。
- - 最终将这些参数移至外部配置文件(如 YAML 或 JSON 文件),方便用户自定义。
-
-- **[0.6.3]提供 HFC (HeartFlowChatInstance) 开启/关闭选项:**
- - 增加一个全局或针对特定子心流的配置选项。
- - 允许用户控制是否启用 `FOCUSED` 状态以及关联的 `HeartFlowChatInstance`。
- - 如果禁用 HFC,子心流可能只会在 `ABSENT` 和 `CHAT` 状态间切换。
-
-- [0.6.3]**添加防破线机制 (针对接收消息):**
- - 在消息处理流程的早期阶段 (例如 `HeartHC_processor` 或类似模块),增加对接收到的消息文本长度的检查。
- - 对超过预设长度阈值的*接收*消息进行截断处理。
- - 目的是防止过长的输入(可能包含"破限"提示词)影响后续的兴趣计算、LLM 回复生成等环节。
-
-- [0.6.3]**NormalChat 模式下的记忆与 Prompt 优化:**
- - 重点审视 `NormalChatInstance` (闲聊/推理模式) 中记忆调用 (例如 `HippocampusManager` 的使用) 的方式。
- - 评估在该模式下引入工具调用 (Tool Calling) 机制以更结构化访问记忆的必要性。
- - 优化 `NormalChatInstance` 中与记忆检索、应用相关的 Prompt。
-
-- [0.6.3]**完善简易兴趣监控 GUI:**
- - 改进现有的、用于监控聊天兴趣度 (`InterestChatting`?) 的简单 GUI 界面。
- - 使其能更清晰地展示关键参数和状态,作为查看日志之外的更直观的监控方式。
- - 作为完整外部 UI 开发完成前的临时替代方案。
-
-- [0.6.3]**修复/完善中期记忆 (Midterm Memory):**
- - 检查当前中期记忆模块的状态。
- - 修复已知问题,使其能够稳定运行。
- - (优先级视开发时间而定)
-
-
-对于有些群频繁激活HFC,却不回复,需要处理一下
diff --git a/src/heart_flow/0.6Bing.md b/src/heart_flow/0.6Bing.md
index 6af057e29..de5628e78 100644
--- a/src/heart_flow/0.6Bing.md
+++ b/src/heart_flow/0.6Bing.md
@@ -81,4 +81,14 @@
- **基于人格生成预设知识:**
- 开发利用 LLM 和人格配置生成背景知识的功能。
- 这些知识应符合角色的行为风格和可能的经历。
- - 作为一种"冷启动"或丰富角色深度的方式。
\ No newline at end of file
+ - 作为一种"冷启动"或丰富角色深度的方式。
+
+
+## 开发计划TODO:LIST
+
+- 人格功能:WIP
+- 对特定对象的侧写功能
+- 图片发送,转发功能:WIP
+- 幽默和meme功能:WIP
+- 小程序转发链接解析
+- 自动生成的回复逻辑,例如自生成的回复方向,回复风格
\ No newline at end of file
diff --git a/src/heart_flow/README.md b/src/heart_flow/README.md
index fdb3a1928..a55f1c973 100644
--- a/src/heart_flow/README.md
+++ b/src/heart_flow/README.md
@@ -106,8 +106,8 @@ c HeartFChatting工作方式
- 负责所有 `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` 状态的数量上限)。
+ - 根据 `Heartflow` 的状态 (`self.mai_state_info`) 和限制条件,激活、停用或调整子心流的状态(例如 `enforce_subheartflow_limits`, `randomly_deactivate_subflows`, `sbhf_absent_into_focus`)。
+ - **新增**: 通过调用 `sbhf_absent_into_chat` 方法,使用 LLM (配置与 `Heartflow` 主 LLM 相同) 评估处于 `ABSENT` 或 `CHAT` 状态的子心流,根据观察到的活动摘要和 `Heartflow` 的当前状态,判断是否应在 `ABSENT` 和 `CHAT` 之间进行转换 (同样受限于 `CHAT` 状态的数量上限)。
- **清理机制**: 通过后台任务 (`BackgroundTaskManager`) 定期调用 `cleanup_inactive_subheartflows` 方法,此方法会识别并**删除**那些处于 `ABSENT` 状态超过一小时 (`INACTIVE_THRESHOLD_SECONDS`) 的子心流实例。
### 1.5. 消息处理与回复流程 (Message Processing vs. Replying Flow)
@@ -155,20 +155,20 @@ c HeartFChatting工作方式
- **初始状态**: 新创建的 `SubHeartflow` 默认为 `ABSENT` 状态。
- **`ABSENT` -> `CHAT` (激活闲聊)**:
- **触发条件**: `Heartflow` 的主状态 (`MaiState`) 允许 `CHAT` 模式,且当前 `CHAT` 状态的子心流数量未达上限。
- - **判定机制**: `SubHeartflowManager` 中的 `evaluate_and_transition_subflows_by_llm` 方法调用大模型(LLM)。LLM 读取该群聊的近期内容和结合自身个性信息,判断是否"想"在该群开始聊天。
+ - **判定机制**: `SubHeartflowManager` 中的 `sbhf_absent_into_chat` 方法调用大模型(LLM)。LLM 读取该群聊的近期内容和结合自身个性信息,判断是否"想"在该群开始聊天。
- **执行**: 若 LLM 判断为是,且名额未满,`SubHeartflowManager` 调用 `change_chat_state(ChatState.CHAT)`。
- **`CHAT` -> `FOCUSED` (激活专注)**:
- **触发条件**: 子心流处于 `CHAT` 状态,其内部维护的"开屎热聊"概率 (`InterestChatting.start_hfc_probability`) 达到预设阈值(表示对当前聊天兴趣浓厚),同时 `Heartflow` 的主状态允许 `FOCUSED` 模式,且 `FOCUSED` 名额未满。
- - **判定机制**: `SubHeartflowManager` 中的 `evaluate_interest_and_promote` 方法定期检查满足条件的 `CHAT` 子心流。
+ - **判定机制**: `SubHeartflowManager` 中的 `sbhf_absent_into_focus` 方法定期检查满足条件的 `CHAT` 子心流。
- **执行**: 若满足所有条件,`SubHeartflowManager` 调用 `change_chat_state(ChatState.FOCUSED)`。
- **注意**: 无法从 `ABSENT` 直接跳到 `FOCUSED`,必须先经过 `CHAT`。
- **`FOCUSED` -> `ABSENT` (退出专注)**:
- - **主要途径 (内部驱动)**: 在 `FOCUSED` 状态下运行的 `HeartFlowChatInstance` 连续多次决策为 `no_reply` (例如达到 5 次,次数可配),它会通过回调函数 (`request_absent_transition`) 请求 `SubHeartflowManager` 将其状态**直接**设置为 `ABSENT`。
+ - **主要途径 (内部驱动)**: 在 `FOCUSED` 状态下运行的 `HeartFlowChatInstance` 连续多次决策为 `no_reply` (例如达到 5 次,次数可配),它会通过回调函数 (`sbhf_focus_into_absent`) 请求 `SubHeartflowManager` 将其状态**直接**设置为 `ABSENT`。
- **其他途径 (外部驱动)**:
- `Heartflow` 主状态变为 `OFFLINE`,`SubHeartflowManager` 强制所有子心流变为 `ABSENT`。
- `SubHeartflowManager` 因 `FOCUSED` 名额超限 (`enforce_subheartflow_limits`) 或随机停用 (`randomly_deactivate_subflows`) 而将其设置为 `ABSENT`。
- **`CHAT` -> `ABSENT` (退出闲聊)**:
- - **主要途径 (内部驱动)**: `SubHeartflowManager` 中的 `evaluate_and_transition_subflows_by_llm` 方法调用 LLM。LLM 读取群聊内容和结合自身状态,判断是否"不想"继续在此群闲聊。
+ - **主要途径 (内部驱动)**: `SubHeartflowManager` 中的 `sbhf_absent_into_chat` 方法调用 LLM。LLM 读取群聊内容和结合自身状态,判断是否"不想"继续在此群闲聊。
- **执行**: 若 LLM 判断为是,`SubHeartflowManager` 调用 `change_chat_state(ChatState.ABSENT)`。
- **其他途径 (外部驱动)**:
- `Heartflow` 主状态变为 `OFFLINE`。
diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py
index fd0163082..56fee2a91 100644
--- a/src/heart_flow/background_tasks.py
+++ b/src/heart_flow/background_tasks.py
@@ -12,10 +12,17 @@ from src.heart_flow.interest_logger import InterestLogger
logger = get_logger("background_tasks")
-# 新增随机停用间隔 (5 分钟)
-RANDOM_DEACTIVATION_INTERVAL_SECONDS = 300
+
# 新增兴趣评估间隔
INTEREST_EVAL_INTERVAL_SECONDS = 5
+# 新增聊天超时检查间隔
+NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS = 60
+# 新增状态评估间隔
+HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS = 60
+
+CLEANUP_INTERVAL_SECONDS = 1200
+STATE_UPDATE_INTERVAL_SECONDS = 60
+LOG_INTERVAL_SECONDS = 3
class BackgroundTaskManager:
@@ -27,33 +34,19 @@ class BackgroundTaskManager:
mai_state_manager: MaiStateManager,
subheartflow_manager: SubHeartflowManager,
interest_logger: InterestLogger,
- update_interval: int,
- cleanup_interval: int,
- log_interval: int,
- # 新增兴趣评估间隔参数
- interest_eval_interval: int = INTEREST_EVAL_INTERVAL_SECONDS,
- # 新增随机停用间隔参数
- random_deactivation_interval: int = RANDOM_DEACTIVATION_INTERVAL_SECONDS,
):
self.mai_state_info = mai_state_info
self.mai_state_manager = mai_state_manager
self.subheartflow_manager = subheartflow_manager
self.interest_logger = interest_logger
- # Intervals
- self.update_interval = update_interval
- self.cleanup_interval = cleanup_interval
- self.log_interval = log_interval
- self.interest_eval_interval = interest_eval_interval # 存储兴趣评估间隔
- self.random_deactivation_interval = random_deactivation_interval # 存储随机停用间隔
-
# Task references
self._state_update_task: Optional[asyncio.Task] = None
self._cleanup_task: Optional[asyncio.Task] = None
self._logging_task: Optional[asyncio.Task] = None
- self._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._normal_chat_timeout_check_task: Optional[asyncio.Task] = None # Nyaa~ 添加聊天超时检查任务的引用
+ self._hf_judge_state_update_task: Optional[asyncio.Task] = None # Nyaa~ 添加状态评估任务的引用
+ self._into_focus_task: Optional[asyncio.Task] = None # Nyaa~ 添加兴趣评估任务的引用
self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks
async def start_tasks(self):
@@ -65,57 +58,53 @@ class BackgroundTaskManager:
- 将任务引用保存到任务列表
"""
- # 任务配置列表: (任务变量名, 任务函数, 任务名称, 日志级别, 额外日志信息, 任务对象引用属性名)
+ # 任务配置列表: (任务函数, 任务名称, 日志级别, 额外日志信息, 任务对象引用属性名)
task_configs = [
(
- self._state_update_task,
- lambda: self._run_state_update_cycle(self.update_interval),
- "hf_state_update",
+ lambda: self._run_state_update_cycle(STATE_UPDATE_INTERVAL_SECONDS),
"debug",
- f"聊天状态更新任务已启动 间隔:{self.update_interval}s",
+ f"聊天状态更新任务已启动 间隔:{STATE_UPDATE_INTERVAL_SECONDS}s",
"_state_update_task",
),
(
- self._hf_judge_state_update_task,
- lambda: self._run_hf_judge_state_update_cycle(60),
- "hf_judge_state_update",
+ lambda: self._run_normal_chat_timeout_check_cycle(NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS),
"debug",
- f"状态评估任务已启动 间隔:{60}s",
+ f"聊天超时检查任务已启动 间隔:{NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS}s",
+ "_normal_chat_timeout_check_task",
+ ),
+ (
+ lambda: self._run_absent_into_chat(HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS),
+ "debug",
+ f"状态评估任务已启动 间隔:{HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS}s",
"_hf_judge_state_update_task",
),
(
- self._cleanup_task,
self._run_cleanup_cycle,
- "hf_cleanup",
"info",
- f"清理任务已启动 间隔:{self.cleanup_interval}s",
+ f"清理任务已启动 间隔:{CLEANUP_INTERVAL_SECONDS}s",
"_cleanup_task",
),
(
- self._logging_task,
self._run_logging_cycle,
- "hf_logging",
"info",
- f"日志任务已启动 间隔:{self.log_interval}s",
+ f"日志任务已启动 间隔:{LOG_INTERVAL_SECONDS}s",
"_logging_task",
),
# 新增兴趣评估任务配置
(
- self._interest_eval_task,
- self._run_interest_eval_cycle,
- "hf_interest_eval",
+ self._run_into_focus_cycle,
"debug", # 设为debug,避免过多日志
- f"兴趣评估任务已启动 间隔:{self.interest_eval_interval}s",
- "_interest_eval_task",
+ f"专注评估任务已启动 间隔:{INTEREST_EVAL_INTERVAL_SECONDS}s",
+ "_into_focus_task",
),
]
# 统一启动所有任务
- for _task_var, task_func, task_name, log_level, log_msg, task_attr_name in task_configs:
+ for task_func, log_level, log_msg, task_attr_name in task_configs:
# 检查任务变量是否存在且未完成
current_task_var = getattr(self, task_attr_name)
if current_task_var is None or current_task_var.done():
- new_task = asyncio.create_task(task_func(), name=task_name)
+ new_task = asyncio.create_task(task_func())
setattr(self, task_attr_name, new_task) # 更新任务变量
if new_task not in self._tasks: # 避免重复添加
self._tasks.append(new_task)
@@ -123,7 +112,7 @@ class BackgroundTaskManager:
# 根据配置记录不同级别的日志
getattr(logger, log_level)(log_msg)
else:
- logger.warning(f"{task_name}任务已在运行")
+ logger.warning(f"{task_attr_name}任务已在运行")
async def stop_tasks(self):
"""停止所有后台任务。
@@ -209,10 +198,15 @@ class BackgroundTaskManager:
logger.info("检测到离线,停用所有子心流")
await self.subheartflow_manager.deactivate_all_subflows()
- async def _perform_hf_judge_state_update_work(self):
+ async def _perform_absent_into_chat(self):
"""调用llm检测是否转换ABSENT-CHAT状态"""
- logger.info("[状态评估任务] 开始基于LLM评估子心流状态...")
- await self.subheartflow_manager.evaluate_and_transition_subflows_by_llm()
+ logger.debug("[状态评估任务] 开始基于LLM评估子心流状态...")
+ await self.subheartflow_manager.sbhf_absent_into_chat()
+
+ async def _normal_chat_timeout_check_work(self):
+ """检查处于CHAT状态的子心流是否因长时间未发言而超时,并将其转为ABSENT"""
+ logger.debug("[聊天超时检查] 开始检查处于CHAT状态的子心流...")
+ await self.subheartflow_manager.sbhf_chat_into_absent()
async def _perform_cleanup_work(self):
"""执行子心流清理任务
@@ -244,10 +238,10 @@ class BackgroundTaskManager:
await self.interest_logger.log_all_states()
# --- 新增兴趣评估工作函数 ---
- async def _perform_interest_eval_work(self):
+ async def _perform_into_focus_work(self):
"""执行一轮子心流兴趣评估与提升检查。"""
# 直接调用 subheartflow_manager 的方法,并传递当前状态信息
- await self.subheartflow_manager.evaluate_interest_and_promote()
+ await self.subheartflow_manager.sbhf_absent_into_focus()
# --- 结束新增 ---
@@ -259,25 +253,30 @@ 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):
+ async def _run_absent_into_chat(self, interval: int):
await self._run_periodic_loop(
- task_name="State Update", interval=interval, task_func=self._perform_hf_judge_state_update_work
+ task_name="Into Chat", interval=interval, task_func=self._perform_absent_into_chat
+ )
+
+ async def _run_normal_chat_timeout_check_cycle(self, interval: int):
+ await self._run_periodic_loop(
+ task_name="Normal Chat Timeout Check", interval=interval, task_func=self._normal_chat_timeout_check_work
)
async def _run_cleanup_cycle(self):
await self._run_periodic_loop(
- task_name="Subflow Cleanup", interval=self.cleanup_interval, task_func=self._perform_cleanup_work
+ task_name="Subflow Cleanup", interval=CLEANUP_INTERVAL_SECONDS, task_func=self._perform_cleanup_work
)
async def _run_logging_cycle(self):
await self._run_periodic_loop(
- task_name="State Logging", interval=self.log_interval, task_func=self._perform_logging_work
+ task_name="State Logging", interval=LOG_INTERVAL_SECONDS, task_func=self._perform_logging_work
)
# --- 新增兴趣评估任务运行器 ---
- async def _run_interest_eval_cycle(self):
+ async def _run_into_focus_cycle(self):
await self._run_periodic_loop(
- task_name="Interest Evaluation",
- interval=self.interest_eval_interval,
- task_func=self._perform_interest_eval_work,
+ task_name="Into Focus",
+ interval=INTEREST_EVAL_INTERVAL_SECONDS,
+ task_func=self._perform_into_focus_work,
)
diff --git a/src/heart_flow/heartflow.py b/src/heart_flow/heartflow.py
index f37b7f81f..bd8bc6ff4 100644
--- a/src/heart_flow/heartflow.py
+++ b/src/heart_flow/heartflow.py
@@ -11,20 +11,10 @@ from src.heart_flow.subheartflow_manager import SubHeartflowManager
from src.heart_flow.mind import Mind
from src.heart_flow.interest_logger import InterestLogger # Import InterestLogger
from src.heart_flow.background_tasks import BackgroundTaskManager # Import BackgroundTaskManager
-# --- End import ---
logger = get_logger("heartflow")
-# Task Intervals (should be in BackgroundTaskManager or config)
-CLEANUP_INTERVAL_SECONDS = 1200
-STATE_UPDATE_INTERVAL_SECONDS = 60
-
-# Thresholds (should be in SubHeartflowManager or config)
-INACTIVE_THRESHOLD_SECONDS = 1200
-# --- End Constants --- #
-
-
class Heartflow:
"""主心流协调器,负责初始化并协调各个子系统:
- 状态管理 (MaiState)
@@ -65,9 +55,6 @@ class Heartflow:
mai_state_manager=self.mai_state_manager,
subheartflow_manager=self.subheartflow_manager,
interest_logger=self.interest_logger,
- update_interval=STATE_UPDATE_INTERVAL_SECONDS,
- cleanup_interval=CLEANUP_INTERVAL_SECONDS,
- log_interval=3, # Example: Using value directly, ideally get from config
)
async def get_or_create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]:
diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py
index 48eead1a1..29277820e 100644
--- a/src/heart_flow/mai_state_manager.py
+++ b/src/heart_flow/mai_state_manager.py
@@ -4,24 +4,33 @@ import random
from typing import List, Tuple, Optional
from src.common.logger_manager import get_logger
from src.plugins.moods.moods import MoodManager
+from src.config.config import global_config
logger = get_logger("mai_state")
# -- 状态相关的可配置参数 (可以从 glocal_config 加载) --
-enable_unlimited_hfc_chat = True # 调试用:无限专注聊天
-# enable_unlimited_hfc_chat = False
-prevent_offline_state = True # 调试用:防止进入离线状态
+# The line `enable_unlimited_hfc_chat = False` is setting a configuration parameter that controls
+# whether a specific debugging feature is enabled or not. When `enable_unlimited_hfc_chat` is set to
+# `False`, it means that the debugging feature for unlimited focused chatting is disabled.
+# enable_unlimited_hfc_chat = True # 调试用:无限专注聊天
+enable_unlimited_hfc_chat = False
+prevent_offline_state = True
+# 目前默认不启用OFFLINE状态
# 不同状态下普通聊天的最大消息数
-MAX_NORMAL_CHAT_NUM_PEEKING = 30
-MAX_NORMAL_CHAT_NUM_NORMAL = 40
-MAX_NORMAL_CHAT_NUM_FOCUSED = 30
+base_normal_chat_num = global_config.base_normal_chat_num
+base_focused_chat_num = global_config.base_focused_chat_num
+
+
+MAX_NORMAL_CHAT_NUM_PEEKING = int(base_normal_chat_num / 2)
+MAX_NORMAL_CHAT_NUM_NORMAL = base_normal_chat_num
+MAX_NORMAL_CHAT_NUM_FOCUSED = base_normal_chat_num + 1
# 不同状态下专注聊天的最大消息数
-MAX_FOCUSED_CHAT_NUM_PEEKING = 20
-MAX_FOCUSED_CHAT_NUM_NORMAL = 30
-MAX_FOCUSED_CHAT_NUM_FOCUSED = 40
+MAX_FOCUSED_CHAT_NUM_PEEKING = int(base_focused_chat_num / 2)
+MAX_FOCUSED_CHAT_NUM_NORMAL = base_focused_chat_num
+MAX_FOCUSED_CHAT_NUM_FOCUSED = base_focused_chat_num + 2
# -- 状态定义 --
@@ -164,7 +173,7 @@ class MaiStateManager:
if random.random() < 0.03: # 3% 概率切换到 OFFLINE
potential_next = MaiState.OFFLINE
resolved_next = _resolve_offline(potential_next)
- logger.debug(f"规则1:概率触发下线,resolve 为 {resolved_next.value}")
+ logger.debug(f"概率触发下线,resolve 为 {resolved_next.value}")
# 只有当解析后的状态与当前状态不同时才设置 next_state
if resolved_next != current_status:
next_state = resolved_next
diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py
index 5793e772f..e34f37d32 100644
--- a/src/heart_flow/observation.py
+++ b/src/heart_flow/observation.py
@@ -146,7 +146,7 @@ class ChattingObservation(Observation):
self.talking_message_str = await build_readable_messages(
messages=self.talking_message,
- timestamp_mode="normal",
+ timestamp_mode="lite",
read_mark=last_obs_time_mark,
)
self.talking_message_str_truncate = await build_readable_messages(
diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py
index f40160681..8d07e6b52 100644
--- a/src/heart_flow/sub_heartflow.py
+++ b/src/heart_flow/sub_heartflow.py
@@ -5,7 +5,6 @@ import time
from typing import Optional, List, Dict, Tuple, Callable, Coroutine
import traceback
from src.common.logger_manager import get_logger
-import random
from src.plugins.chat.message import MessageRecv
from src.plugins.chat.chat_stream import chat_manager
import math
@@ -15,20 +14,15 @@ 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
-logger = get_logger("subheartflow")
+logger = get_logger("sub_heartflow")
-base_reply_probability = 0.05
-probability_increase_rate_per_second = 0.08
-max_reply_probability = 1
+PROBABILITY_INCREASE_RATE_PER_SECOND = 0.1
+PROBABILITY_DECREASE_RATE_PER_SECOND = 0.1
+MAX_REPLY_PROBABILITY = 1
class InterestChatting:
@@ -37,24 +31,15 @@ class InterestChatting:
decay_rate=global_config.default_decay_rate_per_second,
max_interest=MAX_INTEREST,
trigger_threshold=global_config.reply_trigger_threshold,
- base_reply_probability=base_reply_probability,
- increase_rate=probability_increase_rate_per_second,
- decay_factor=global_config.probability_decay_factor_per_second,
- max_probability=max_reply_probability,
+ max_probability=MAX_REPLY_PROBABILITY,
):
# 基础属性初始化
self.interest_level: float = 0.0
- self.last_update_time: float = time.time()
self.decay_rate_per_second: float = decay_rate
self.max_interest: float = max_interest
- self.last_interaction_time: float = self.last_update_time
self.trigger_threshold: float = trigger_threshold
- self.base_reply_probability: float = base_reply_probability
- self.probability_increase_rate: float = increase_rate
- self.probability_decay_factor: float = decay_factor
self.max_reply_probability: float = max_probability
- self.current_reply_probability: float = 0.0
self.is_above_threshold: bool = False
# 任务相关属性初始化
@@ -100,7 +85,6 @@ class InterestChatting:
"""
# 添加新消息
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:
@@ -144,10 +128,10 @@ class InterestChatting:
async def _update_reply_probability(self):
self.above_threshold = self.interest_level >= self.trigger_threshold
if self.above_threshold:
- self.start_hfc_probability += 0.1
+ self.start_hfc_probability += PROBABILITY_INCREASE_RATE_PER_SECOND
else:
if self.start_hfc_probability > 0:
- self.start_hfc_probability = max(0, self.start_hfc_probability - 0.1)
+ self.start_hfc_probability = max(0, self.start_hfc_probability - PROBABILITY_DECREASE_RATE_PER_SECOND)
async def increase_interest(self, value: float):
self.interest_level += value
@@ -168,13 +152,6 @@ class InterestChatting:
"above_threshold": self.above_threshold,
}
- async def should_evaluate_reply(self) -> bool:
- if self.current_reply_probability > 0:
- trigger = random.random() < self.current_reply_probability
- return trigger
- else:
- return False
-
# --- 新增后台更新任务相关方法 ---
async def _run_update_loop(self, update_interval: float = 1.0):
"""后台循环,定期更新兴趣和回复概率。"""
@@ -322,7 +299,7 @@ class SubHeartflow:
chat_stream = chat_manager.get_stream(self.chat_id)
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} 开始普通聊天,随便水群...")
await self.normal_chat_instance.start_chat() # <--- 修正:调用 start_chat
return True
except Exception as e:
@@ -334,7 +311,7 @@ class SubHeartflow:
async def _stop_heart_fc_chat(self):
"""停止并清理 HeartFChatting 实例"""
if self.heart_fc_instance:
- logger.info(f"{self.log_prefix} 关闭 HeartFChatting 实例...")
+ logger.debug(f"{self.log_prefix} 结束专注聊天...")
try:
await self.heart_fc_instance.shutdown()
except Exception as e:
@@ -369,7 +346,7 @@ class SubHeartflow:
return True # 已经在运行
# 如果实例不存在,则创建并启动
- logger.info(f"{log_prefix} 麦麦准备开始专注聊天 (创建新实例)...")
+ logger.info(f"{log_prefix} 麦麦准备开始专注聊天...")
try:
# 创建 HeartFChatting 实例,并传递 从构造函数传入的 回调函数
self.heart_fc_instance = HeartFChatting(
@@ -382,7 +359,7 @@ class SubHeartflow:
# 初始化并启动 HeartFChatting
if await self.heart_fc_instance._initialize():
await self.heart_fc_instance.start()
- logger.info(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。")
+ logger.debug(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。")
return True
else:
logger.error(f"{log_prefix} HeartFChatting 初始化失败,无法进入专注模式。")
@@ -409,7 +386,7 @@ class SubHeartflow:
# 移除限额检查逻辑
logger.debug(f"{log_prefix} 准备进入或保持 聊天 状态")
if await self._start_normal_chat():
- logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。")
+ # logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。")
state_changed = True
else:
logger.error(f"{log_prefix} 启动 NormalChat 失败,无法进入 CHAT 状态。")
@@ -420,7 +397,7 @@ class SubHeartflow:
# 移除限额检查逻辑
logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态")
if await self._start_heart_fc_chat():
- logger.info(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。")
+ logger.debug(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。")
state_changed = True
else:
logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。")
@@ -439,7 +416,7 @@ class SubHeartflow:
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}"
+ f"{log_prefix} 麦麦的聊天状态从 {current_state.value} (持续了 {int(self.chat_state_last_time)} 秒) 变更为 {new_state.value}"
)
self.chat_state.chat_status = new_state
@@ -493,11 +470,10 @@ class SubHeartflow:
async def get_interest_state(self) -> dict:
return await self.interest_chatting.get_state()
- async def get_interest_level(self) -> float:
- return await self.interest_chatting.get_interest()
-
- async def should_evaluate_reply(self) -> bool:
- return await self.interest_chatting.should_evaluate_reply()
+ def get_normal_chat_last_speak_time(self) -> float:
+ if self.normal_chat_instance:
+ return self.normal_chat_instance.last_speak_time
+ return 0
def get_interest_dict(self) -> Dict[str, tuple[MessageRecv, float, bool]]:
return self.interest_chatting.interest_dict
@@ -535,12 +511,12 @@ class SubHeartflow:
# 取消可能存在的旧后台任务 (self.task)
if self.task and not self.task.done():
- logger.info(f"{self.log_prefix} 取消子心流主任务 (Shutdown)...")
+ logger.debug(f"{self.log_prefix} 取消子心流主任务 (Shutdown)...")
self.task.cancel()
try:
await asyncio.wait_for(self.task, timeout=1.0) # 给点时间响应取消
except asyncio.CancelledError:
- logger.info(f"{self.log_prefix} 子心流主任务已取消 (Shutdown)。")
+ logger.debug(f"{self.log_prefix} 子心流主任务已取消 (Shutdown)。")
except asyncio.TimeoutError:
logger.warning(f"{self.log_prefix} 等待子心流主任务取消超时 (Shutdown)。")
except Exception as e:
diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py
index e59ee855c..fbf1be870 100644
--- a/src/heart_flow/sub_mind.py
+++ b/src/heart_flow/sub_mind.py
@@ -140,11 +140,11 @@ class SubMind:
individuality = Individuality.get_instance()
relation_prompt = ""
- print(f"person_list: {person_list}")
+ # print(f"person_list: {person_list}")
for person in person_list:
relation_prompt += await relationship_manager.build_relationship_info(person, is_id=True)
- print(f"relat22222ion_prompt: {relation_prompt}")
+ # print(f"relat22222ion_prompt: {relation_prompt}")
# 构建个性部分
prompt_personality = individuality.get_prompt(x_person=2, level=2)
diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py
index afa0328e6..16f36dcc5 100644
--- a/src/heart_flow/subheartflow_manager.py
+++ b/src/heart_flow/subheartflow_manager.py
@@ -1,7 +1,7 @@
import asyncio
import time
import random
-from typing import Dict, Any, Optional, List
+from typing import Dict, Any, Optional, List, Tuple
import json # 导入 json 模块
import functools # <-- 新增导入
@@ -29,6 +29,7 @@ logger = get_logger("subheartflow_manager")
# 子心流管理相关常量
INACTIVE_THRESHOLD_SECONDS = 3600 # 子心流不活跃超时时间(秒)
+NORMAL_CHAT_TIMEOUT_SECONDS = 30 * 60 # 30分钟
class SubHeartflowManager:
@@ -256,7 +257,7 @@ class SubHeartflowManager:
f"{log_prefix} 完成,共处理 {processed_count} 个子心流,成功将 {changed_count} 个非 ABSENT 子心流的状态更改为 ABSENT。"
)
- async def evaluate_interest_and_promote(self):
+ async def sbhf_absent_into_focus(self):
"""评估子心流兴趣度,满足条件且未达上限则提升到FOCUSED状态(基于start_hfc_probability)"""
try:
log_prefix = "[兴趣评估]"
@@ -271,10 +272,7 @@ class SubHeartflowManager:
return # 如果不允许,直接返回
# --- 结束新增 ---
- 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}个群激情聊天")
+ logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 可以在{focused_limit}个群激情聊天")
if focused_limit <= 0:
# logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 不允许 FOCUSED 子心流")
@@ -333,139 +331,207 @@ class SubHeartflowManager:
except Exception as e:
logger.error(f"启动HFC 兴趣评估失败: {e}", exc_info=True)
- async def evaluate_and_transition_subflows_by_llm(self):
+ async def sbhf_absent_into_chat(self):
"""
- 使用LLM评估每个子心流的状态,并根据LLM的判断执行状态转换(ABSENT <-> CHAT)。
- 注意:此函数包含对假设的LLM函数的调用。
+ 随机选一个 ABSENT 状态的子心流,评估是否应转换为 CHAT 状态。
+ 每次调用最多转换一个。
"""
- # 获取当前状态和限制,用于CHAT激活检查
current_mai_state = self.mai_state_info.get_current_state()
chat_limit = current_mai_state.get_normal_chat_max_num()
- transitioned_to_chat = 0
- transitioned_to_absent = 0
+ async with self._lock:
+ # 1. 筛选出所有 ABSENT 状态的子心流
+ absent_subflows = [
+ hf for hf in self.subheartflows.values() if hf.chat_state.chat_status == ChatState.ABSENT
+ ]
- async with self._lock: # 在锁内获取快照并迭代
- subflows_snapshot = list(self.subheartflows.values())
- # 使用不上锁的版本,因为我们已经在锁内
+ if not absent_subflows:
+ logger.debug("没有摸鱼的子心流可以评估。") # 日志太频繁,注释掉
+ return # 没有目标,直接返回
+
+ # 2. 随机选一个幸运儿
+ sub_hf_to_evaluate = random.choice(absent_subflows)
+ flow_id = sub_hf_to_evaluate.subheartflow_id
+ stream_name = chat_manager.get_stream_name(flow_id) or flow_id
+ log_prefix = f"[{stream_name}]"
+
+ # 3. 检查 CHAT 上限
current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
+ if current_chat_count >= chat_limit:
+ logger.info(f"{log_prefix} 想看看能不能聊,但是聊天太多了, ({current_chat_count}/{chat_limit}) 满了。")
+ return # 满了,这次就算了
+
+ # --- 获取 FOCUSED 计数 ---
+ current_focused_count = self.count_subflows_by_state_nolock(ChatState.FOCUSED)
+ focused_limit = current_mai_state.get_focused_chat_max_num()
+
+ # --- 新增:获取聊天和专注群名 ---
+ chatting_group_names = []
+ focused_group_names = []
+ for flow_id, hf in self.subheartflows.items():
+ stream_name = chat_manager.get_stream_name(flow_id) or str(flow_id) # 保证有名字
+ if hf.chat_state.chat_status == ChatState.CHAT:
+ chatting_group_names.append(stream_name)
+ elif hf.chat_state.chat_status == ChatState.FOCUSED:
+ focused_group_names.append(stream_name)
+ # --- 结束新增 ---
+
+ # --- 获取观察信息和构建 Prompt ---
+ first_observation = sub_hf_to_evaluate.observations[0] # 喵~第一个观察者肯定存在的说
+ await first_observation.observe()
+ current_chat_log = first_observation.talking_message_str or "当前没啥聊天内容。"
+ _observation_summary = f"最近聊了这些:\n{current_chat_log}"
+
+ mai_state_description = f"你当前状态: {current_mai_state.value}。"
+ individuality = Individuality.get_instance()
+ personality_prompt = individuality.get_prompt(x_person=2, level=2)
+ prompt_personality = f"你正在扮演名为{individuality.name}的人类,{personality_prompt}"
+
+ # --- 修改:在 prompt 中加入当前聊天计数和群名信息 (条件显示) ---
+ chat_status_lines = []
+ if chatting_group_names:
+ chat_status_lines.append(
+ f"正在闲聊 ({current_chat_count}/{chat_limit}): {', '.join(chatting_group_names)}"
+ )
+ if focused_group_names:
+ chat_status_lines.append(
+ f"正在专注 ({current_focused_count}/{focused_limit}): {', '.join(focused_group_names)}"
+ )
+
+ chat_status_prompt = "当前没有在任何群聊中。" # 默认消息喵~
+ if chat_status_lines:
+ chat_status_prompt = "当前聊天情况:\n" + "\n".join(chat_status_lines) # 拼接状态信息
+
+ prompt = (
+ f"{prompt_personality}\\n"
+ f"你当前没在 [{stream_name}] 群聊天。\\n"
+ f"{mai_state_description}\\n"
+ f"{chat_status_prompt}\\n" # <-- 喵!用了新的状态信息~
+ f"{_observation_summary}\\n---\\n"
+ f"基于以上信息,你想不想开始在这个群闲聊?\\n"
+ f"请说明理由,并以 JSON 格式回答,包含 'decision' (布尔值) 和 'reason' (字符串)。\\n"
+ f'例如:{{"decision": true, "reason": "看起来挺热闹的,插个话"}}\\n'
+ f'例如:{{"decision": false, "reason": "已经聊了好多,休息一下"}}\\n'
+ f"请只输出有效的 JSON 对象。"
+ )
+ # --- 结束修改 ---
+
+ # --- 4. LLM 评估是否想聊 ---
+ yao_kai_shi_liao_ma, reason = await self._llm_evaluate_state_transition(prompt)
+
+ if reason:
+ if yao_kai_shi_liao_ma:
+ logger.info(f"{log_prefix} 打算开始聊,原因是: {reason}")
+ else:
+ logger.info(f"{log_prefix} 不打算聊,原因是: {reason}")
+ else:
+ logger.info(f"{log_prefix} 结果: {yao_kai_shi_liao_ma}")
+
+ if yao_kai_shi_liao_ma is None:
+ logger.debug(f"{log_prefix} 问AI想不想聊失败了,这次算了。")
+ return # 评估失败,结束
+
+ if not yao_kai_shi_liao_ma:
+ # logger.info(f"{log_prefix} 现在不想聊这个群。")
+ return # 不想聊,结束
+
+ # --- 5. AI想聊,再次检查额度并尝试转换 ---
+ # 再次检查以防万一
+ current_chat_count_before_change = self.count_subflows_by_state_nolock(ChatState.CHAT)
+ if current_chat_count_before_change < chat_limit:
+ logger.info(
+ f"{log_prefix} 想聊,而且还有精力 ({current_chat_count_before_change}/{chat_limit}),这就去聊!"
+ )
+ await sub_hf_to_evaluate.change_chat_state(ChatState.CHAT)
+ # 确认转换成功
+ if sub_hf_to_evaluate.chat_state.chat_status == ChatState.CHAT:
+ logger.debug(f"{log_prefix} 成功进入聊天状态!本次评估圆满结束。")
+ else:
+ logger.warning(
+ f"{log_prefix} 奇怪,尝试进入聊天状态失败了。当前状态: {sub_hf_to_evaluate.chat_state.chat_status.value}"
+ )
+ else:
+ logger.warning(
+ f"{log_prefix} AI说想聊,但是刚问完就没空位了 ({current_chat_count_before_change}/{chat_limit})。真不巧,下次再说吧。"
+ )
+ # 无论转换成功与否,本次评估都结束了
+
+ # 锁在这里自动释放
+
+ # --- 新增:单独检查 CHAT 状态超时的任务 ---
+ async def sbhf_chat_into_absent(self):
+ """定期检查处于 CHAT 状态的子心流是否因长时间未发言而超时,并将其转为 ABSENT。"""
+ log_prefix_task = "[聊天超时检查]"
+ transitioned_to_absent = 0
+ checked_count = 0
+
+ async with self._lock:
+ subflows_snapshot = list(self.subheartflows.values())
+ checked_count = len(subflows_snapshot)
if not subflows_snapshot:
- logger.info("当前没有子心流需要评估。")
+ # logger.debug(f"{log_prefix_task} 没有子心流需要检查超时。")
return
for sub_hf in subflows_snapshot:
+ # 只检查 CHAT 状态的子心流
+ if sub_hf.chat_state.chat_status != ChatState.CHAT:
+ continue
+
flow_id = sub_hf.subheartflow_id
stream_name = chat_manager.get_stream_name(flow_id) or flow_id
- log_prefix = f"[{stream_name}]"
- current_subflow_state = sub_hf.chat_state.chat_status
+ log_prefix = f"[{stream_name}]({log_prefix_task})"
- _observation_summary = "没有可用的观察信息。" # 默认值
+ should_deactivate = False
+ reason = ""
- 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 类型。")
+ try:
+ # 使用变量名 last_bot_dong_zuo_time 替代 last_bot_activity_time
+ last_bot_dong_zuo_time = sub_hf.get_normal_chat_last_speak_time()
- # --- 获取麦麦状态 ---
- mai_state_description = f"你当前状态: {current_mai_state.value}。"
+ if last_bot_dong_zuo_time > 0:
+ current_time = time.time()
+ # 使用变量名 time_since_last_bb 替代 time_since_last_reply
+ time_since_last_bb = current_time - last_bot_dong_zuo_time
- # 获取个性化信息
- 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 对象。"
- )
-
- # 调用LLM评估
- should_activate = await self._llm_evaluate_state_transition(prompt)
- if should_activate is None: # 处理解析失败或意外情况
- logger.warning(f"{log_prefix}LLM评估返回无效结果,跳过。")
- continue
-
- if should_activate:
- # 检查CHAT限额
- # 使用不上锁的版本,因为我们已经在锁内
- current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
- if current_chat_count < chat_limit:
+ if time_since_last_bb > NORMAL_CHAT_TIMEOUT_SECONDS:
+ should_deactivate = True
+ reason = f"超过 {NORMAL_CHAT_TIMEOUT_SECONDS / 60:.0f} 分钟没 BB"
logger.info(
- f"{log_prefix}LLM建议激活到CHAT状态,且未达上限({current_chat_count}/{chat_limit})。正在尝试转换..."
+ f"{log_prefix} 太久没有发言 ({reason}),不看了。上次活动时间: {last_bot_dong_zuo_time:.0f}"
)
- 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.info(
- f"{log_prefix}LLM建议激活到CHAT状态,但已达到上限({current_chat_count}/{chat_limit})。跳过转换。"
- )
- else:
- logger.info(f"{log_prefix}LLM建议不激活到CHAT状态。")
+ # else:
+ # logger.debug(f"{log_prefix} Bot活动时间未超时 ({time_since_last_bb:.0f}s < {NORMAL_CHAT_TIMEOUT_SECONDS}s),保持 CHAT 状态。")
+ # else:
+ # 如果没有记录到Bot的活动时间,暂时不因为超时而转换状态
+ # logger.debug(f"{log_prefix} 未找到有效的 Bot 最后活动时间记录,不执行超时检查。")
- # --- 针对 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 对象。"
+ except AttributeError:
+ logger.error(
+ f"{log_prefix} 无法获取 Bot 最后 BB 时间,请确保 SubHeartflow 相关实现正确。跳过超时检查。"
)
+ except Exception as e:
+ logger.error(f"{log_prefix} 检查 Bot 超时状态时出错: {e}", exc_info=True)
- # 调用LLM评估
- should_deactivate = await self._llm_evaluate_state_transition(prompt)
- if should_deactivate is None: # 处理解析失败或意外情况
- logger.warning(f"{log_prefix}LLM评估返回无效结果,跳过。")
- continue
-
- 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
+ # --- 执行状态转换(如果超时) ---
+ if should_deactivate:
+ logger.debug(f"{log_prefix} 因超时 ({reason}),尝试转换为 ABSENT 状态。")
+ await sub_hf.change_chat_state(ChatState.ABSENT)
+ # 再次检查确保状态已改变
+ if sub_hf.chat_state.chat_status == ChatState.ABSENT:
+ transitioned_to_absent += 1
+ logger.info(f"{log_prefix} 不看了。")
else:
- logger.info(f"{log_prefix}LLM建议不进入ABSENT状态。")
+ logger.warning(f"{log_prefix} 尝试因超时转换为 ABSENT 失败。")
- async def _llm_evaluate_state_transition(self, prompt: str) -> Optional[bool]:
+ if transitioned_to_absent > 0:
+ logger.debug(
+ f"{log_prefix_task} 完成,共检查 {checked_count} 个子心流,{transitioned_to_absent} 个因超时转为 ABSENT。"
+ )
+
+ # --- 结束新增 ---
+
+ async def _llm_evaluate_state_transition(self, prompt: str) -> Tuple[Optional[bool], Optional[str]]:
"""
使用 LLM 评估是否应进行状态转换,期望 LLM 返回 JSON 格式。
@@ -482,7 +548,7 @@ class SubHeartflowManager:
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}")
+ logger.debug(f"{log_prefix} 原始评估结果: {response_text}")
# --- 解析 JSON 响应 ---
try:
@@ -493,34 +559,36 @@ class SubHeartflowManager:
data = json.loads(cleaned_response)
decision = data.get("decision") # 使用 .get() 避免 KeyError
+ reason = data.get("reason")
if isinstance(decision, bool):
logger.debug(f"{log_prefix} LLM评估结果 (来自JSON): {'建议转换' if decision else '建议不转换'}")
- return decision
+
+ return decision, reason
else:
logger.warning(
f"{log_prefix} LLM 返回的 JSON 中 'decision' 键的值不是布尔型: {decision}。响应: {response_text}"
)
- return None # 值类型不正确
+ return None, None # 值类型不正确
except json.JSONDecodeError as json_err:
logger.warning(f"{log_prefix} LLM 返回的响应不是有效的 JSON: {json_err}。响应: {response_text}")
# 尝试在非JSON响应中查找关键词作为后备方案 (可选)
if "true" in response_text.lower():
logger.debug(f"{log_prefix} 在非JSON响应中找到 'true',解释为建议转换")
- return True
+ return True, None
if "false" in response_text.lower():
logger.debug(f"{log_prefix} 在非JSON响应中找到 'false',解释为建议不转换")
- return False
- return None # JSON 解析失败,也未找到关键词
+ return False, None
+ return None, None # JSON 解析失败,也未找到关键词
except Exception as parse_err: # 捕获其他可能的解析错误
logger.warning(f"{log_prefix} 解析 LLM JSON 响应时发生意外错误: {parse_err}。响应: {response_text}")
- return None
+ return None, None
except Exception as e:
logger.error(f"{log_prefix} 调用 LLM 或处理其响应时出错: {e}", exc_info=True)
traceback.print_exc()
- return None # LLM 调用或处理失败
+ return None, None # LLM 调用或处理失败
def count_subflows_by_state(self, state: ChatState) -> int:
"""统计指定状态的子心流数量"""
@@ -579,14 +647,14 @@ class SubHeartflowManager:
# --- 新增:处理 HFC 无回复回调的专用方法 --- #
async def _handle_hfc_no_reply(self, subheartflow_id: Any):
"""处理来自 HeartFChatting 的连续无回复信号 (通过 partial 绑定 ID)"""
- # 注意:这里不需要再获取锁,因为 request_absent_transition 内部会处理锁
+ # 注意:这里不需要再获取锁,因为 sbhf_focus_into_absent 内部会处理锁
logger.debug(f"[管理器 HFC 处理器] 接收到来自 {subheartflow_id} 的 HFC 无回复信号")
- await self.request_absent_transition(subheartflow_id)
+ await self.sbhf_focus_into_absent(subheartflow_id)
# --- 结束新增 --- #
# --- 新增:处理来自 HeartFChatting 的状态转换请求 --- #
- async def request_absent_transition(self, subflow_id: Any):
+ async def sbhf_focus_into_absent(self, subflow_id: Any):
"""
接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT。
通常在连续多次 "no_reply" 后被调用。
@@ -606,12 +674,52 @@ class SubHeartflowManager:
# 仅当子心流处于 FOCUSED 状态时才进行转换
# 因为 HeartFChatting 只在 FOCUSED 状态下运行
if current_state == ChatState.FOCUSED:
- logger.info(f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 转换为 ABSENT")
+ target_state = ChatState.ABSENT # 默认目标状态
+ log_reason = "默认转换"
+
+ # 决定是去 ABSENT 还是 CHAT
+ if random.random() < 0.5:
+ target_state = ChatState.ABSENT
+ log_reason = "随机选择 ABSENT"
+ logger.debug(f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 ABSENT")
+ else:
+ # 尝试进入 CHAT,先检查限制
+ current_mai_state = self.mai_state_info.get_current_state()
+ chat_limit = current_mai_state.get_normal_chat_max_num()
+ # 使用不上锁的版本,因为我们已经在锁内
+ current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT)
+
+ if current_chat_count < chat_limit:
+ target_state = ChatState.CHAT
+ log_reason = f"随机选择 CHAT (当前 {current_chat_count}/{chat_limit})"
+ logger.debug(
+ f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT,未达上限 ({current_chat_count}/{chat_limit})"
+ )
+ else:
+ target_state = ChatState.ABSENT
+ log_reason = f"随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT"
+ logger.debug(
+ f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT,但已达上限 ({current_chat_count}/{chat_limit}),改为进入 ABSENT"
+ )
+
+ # 开始转换
+ logger.info(
+ f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 尝试转换为 {target_state.value} ({log_reason})"
+ )
try:
- await subflow.change_chat_state(ChatState.ABSENT)
- logger.info(f"[状态转换请求] {stream_name} 状态已成功转换为 ABSENT")
+ await subflow.change_chat_state(target_state)
+ # 检查最终状态
+ final_state = subflow.chat_state.chat_status
+ if final_state == target_state:
+ logger.debug(f"[状态转换请求] {stream_name} 状态已成功转换为 {final_state.value}")
+ else:
+ logger.warning(
+ f"[状态转换请求] 尝试将 {stream_name} 转换为 {target_state.value} 后,状态实际为 {final_state.value}"
+ )
except Exception as e:
- logger.error(f"[状态转换请求] 转换 {stream_name} 到 ABSENT 时出错: {e}", exc_info=True)
+ logger.error(
+ f"[状态转换请求] 转换 {stream_name} 到 {target_state.value} 时出错: {e}", exc_info=True
+ )
elif current_state == ChatState.ABSENT:
logger.debug(f"[状态转换请求] {stream_name} 已处于 ABSENT 状态,无需转换")
else:
diff --git a/src/individuality/individuality.py b/src/individuality/individuality.py
index 65e9937c2..86e5b63ed 100644
--- a/src/individuality/individuality.py
+++ b/src/individuality/individuality.py
@@ -191,7 +191,7 @@ class Individuality:
获取合并的个体特征prompt
Args:
- level (int): 详细程度 (1: 核心/随机细节, 2: 核心+侧面/细节+其他, 3: 全部)
+ level (int): 详细程度 (1: 核心/随机细节, 2: 核心+随机侧面/全部细节, 3: 全部)
x_person (int, optional): 人称代词 (0: 无人称, 1: 我, 2: 你). 默认为 2.
Returns:
diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py
index a80e96b15..23de9f0d3 100644
--- a/src/plugins/PFC/action_planner.py
+++ b/src/plugins/PFC/action_planner.py
@@ -21,6 +21,7 @@ PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊,
【当前对话目标】
{goals_str}
+{knowledge_info_str}
【最近行动历史概要】
{action_history_summary}
@@ -33,7 +34,7 @@ PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊,
------
可选行动类型以及解释:
-fetch_knowledge: 需要调取知识,当需要专业知识或特定信息时选择,对方若提到你不太认识的人名或实体也可以尝试选择
+fetch_knowledge: 需要调取知识或记忆,当需要专业知识或特定信息时选择,对方若提到你不太认识的人名或实体也可以尝试选择
listening: 倾听对方发言,当你认为对方话才说到一半,发言明显未结束时选择
direct_reply: 直接回复对方
rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择
@@ -53,6 +54,7 @@ PROMPT_FOLLOW_UP = """{persona_text}。现在你在参与一场QQ私聊,刚刚
【当前对话目标】
{goals_str}
+{knowledge_info_str}
【最近行动历史概要】
{action_history_summary}
@@ -224,6 +226,41 @@ class ActionPlanner:
logger.error(f"[私聊][{self.private_name}]构建对话目标字符串时出错: {e}")
goals_str = "- 构建对话目标时出错。\n"
+ # --- 知识信息字符串构建开始 ---
+ knowledge_info_str = "【已获取的相关知识和记忆】\n"
+ try:
+ # 检查 conversation_info 是否有 knowledge_list 并且不为空
+ if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list:
+ # 最多只显示最近的 5 条知识,防止 Prompt 过长
+ recent_knowledge = conversation_info.knowledge_list[-5:]
+ for i, knowledge_item in enumerate(recent_knowledge):
+ if isinstance(knowledge_item, dict):
+ query = knowledge_item.get("query", "未知查询")
+ knowledge = knowledge_item.get("knowledge", "无知识内容")
+ source = knowledge_item.get("source", "未知来源")
+ # 只取知识内容的前 2000 个字,避免太长
+ knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge
+ knowledge_info_str += (
+ f"{i + 1}. 关于 '{query}' 的知识 (来源: {source}):\n {knowledge_snippet}\n"
+ )
+ else:
+ # 处理列表里不是字典的异常情况
+ knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n"
+
+ if not recent_knowledge: # 如果 knowledge_list 存在但为空
+ knowledge_info_str += "- 暂无相关知识和记忆。\n"
+
+ else:
+ # 如果 conversation_info 没有 knowledge_list 属性,或者列表为空
+ knowledge_info_str += "- 暂无相关知识记忆。\n"
+ except AttributeError:
+ logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。")
+ knowledge_info_str += "- 获取知识列表时出错。\n"
+ except Exception as e:
+ logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}")
+ knowledge_info_str += "- 处理知识列表时出错。\n"
+ # --- 知识信息字符串构建结束 ---
+
# 获取聊天历史记录 (chat_history_text)
chat_history_text = ""
try:
@@ -349,6 +386,7 @@ class ActionPlanner:
time_since_last_bot_message_info=time_since_last_bot_message_info,
timeout_context=timeout_context,
chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。",
+ knowledge_info_str=knowledge_info_str,
)
logger.debug(f"[私聊][{self.private_name}]发送到LLM的最终提示词:\n------\n{prompt}\n------")
diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py
index c1b314266..9f744c30c 100644
--- a/src/plugins/PFC/conversation.py
+++ b/src/plugins/PFC/conversation.py
@@ -525,9 +525,9 @@ class Conversation:
)
action_successful = True
except Exception as fetch_err:
- logger.error(f"[私聊][{self.private_name}]获取知识时出错: {fetch_err}")
+ logger.error(f"[私聊][{self.private_name}]获取知识时出错: {str(fetch_err)}")
conversation_info.done_action[action_index].update(
- {"status": "recall", "final_reason": f"获取知识失败: {fetch_err}"}
+ {"status": "recall", "final_reason": f"获取知识失败: {str(fetch_err)}"}
)
self.conversation_info.last_successful_reply_action = None # 重置状态
diff --git a/src/plugins/PFC/message_storage.py b/src/plugins/PFC/message_storage.py
index 55bccb14e..b57f5d2b5 100644
--- a/src/plugins/PFC/message_storage.py
+++ b/src/plugins/PFC/message_storage.py
@@ -50,21 +50,18 @@ class MessageStorage(ABC):
class MongoDBMessageStorage(MessageStorage):
"""MongoDB消息存储实现"""
- def __init__(self):
- self.db = db
-
async def get_messages_after(self, chat_id: str, message_time: float) -> List[Dict[str, Any]]:
query = {"chat_id": chat_id}
# print(f"storage_check_message: {message_time}")
query["time"] = {"$gt": message_time}
- return list(self.db.messages.find(query).sort("time", 1))
+ return list(db.messages.find(query).sort("time", 1))
async def get_messages_before(self, chat_id: str, time_point: float, limit: int = 5) -> List[Dict[str, Any]]:
query = {"chat_id": chat_id, "time": {"$lt": time_point}}
- messages = list(self.db.messages.find(query).sort("time", -1).limit(limit))
+ messages = list(db.messages.find(query).sort("time", -1).limit(limit))
# 将消息按时间正序排列
messages.reverse()
@@ -73,7 +70,7 @@ class MongoDBMessageStorage(MessageStorage):
async def has_new_messages(self, chat_id: str, after_time: float) -> bool:
query = {"chat_id": chat_id, "time": {"$gt": after_time}}
- return self.db.messages.find_one(query) is not None
+ return db.messages.find_one(query) is not None
# # 创建一个内存消息存储实现,用于测试
diff --git a/src/plugins/PFC/pfc_KnowledgeFetcher.py b/src/plugins/PFC/pfc_KnowledgeFetcher.py
index 099b4979e..0989339df 100644
--- a/src/plugins/PFC/pfc_KnowledgeFetcher.py
+++ b/src/plugins/PFC/pfc_KnowledgeFetcher.py
@@ -68,16 +68,18 @@ class KnowledgeFetcher:
max_depth=3,
fast_retrieval=False,
)
- knowledge = ""
+ knowledge_text = ""
+ sources_text = "无记忆匹配" # 默认值
if related_memory:
sources = []
for memory in related_memory:
- knowledge += memory[1] + "\n"
+ knowledge_text += memory[1] + "\n"
sources.append(f"记忆片段{memory[0]}")
- knowledge = knowledge.strip(), ",".join(sources)
+ knowledge_text = knowledge_text.strip()
+ sources_text = ",".join(sources)
- knowledge += "现在有以下**知识**可供参考:\n "
- knowledge += self._lpmm_get_knowledge(query)
- knowledge += "请记住这些**知识**,并根据**知识**回答问题。\n"
+ knowledge_text += "\n现在有以下**知识**可供参考:\n "
+ knowledge_text += self._lpmm_get_knowledge(query)
+ knowledge_text += "\n请记住这些**知识**,并根据**知识**回答问题。\n"
- return "未找到相关知识", "无记忆匹配"
+ return knowledge_text or "未找到相关知识", sources_text or "无记忆匹配"
diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py
index 9b497ef28..890f807c7 100644
--- a/src/plugins/PFC/reply_generator.py
+++ b/src/plugins/PFC/reply_generator.py
@@ -17,6 +17,9 @@ logger = get_module_logger("reply_generator")
PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请根据以下信息生成一条回复:
当前对话目标:{goals_str}
+
+{knowledge_info_str}
+
最近的聊天记录:
{chat_history_text}
@@ -25,7 +28,7 @@ PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请
1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!)
2. 符合你的性格特征和身份细节
3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况)
-4. 适当利用相关知识,但不要生硬引用
+4. 可以适当利用相关知识,但不要生硬引用
5. 自然、得体,结合聊天记录逻辑合理,且没有重复表达同质内容
请注意把握聊天内容,不要回复的太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。
@@ -39,6 +42,9 @@ PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请
PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊,**刚刚你已经发送了一条或多条消息**,现在请根据以下信息再发一条新消息:
当前对话目标:{goals_str}
+
+{knowledge_info_str}
+
最近的聊天记录:
{chat_history_text}
@@ -47,7 +53,7 @@ PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊
1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!)
2. 符合你的性格特征和身份细节
3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况)
-4. 适当利用相关知识,但不要生硬引用
+4. 可以适当利用相关知识,但不要生硬引用
5. 跟之前你发的消息自然的衔接,逻辑合理,且没有重复表达同质内容或部分重叠内容
请注意把握聊天内容,不用太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。
@@ -131,6 +137,38 @@ class ReplyGenerator:
else:
goals_str = "- 目前没有明确对话目标\n" # 简化无目标情况
+ # --- 新增:构建知识信息字符串 ---
+ knowledge_info_str = "【供参考的相关知识和记忆】\n" # 稍微改下标题,表明是供参考
+ try:
+ # 检查 conversation_info 是否有 knowledge_list 并且不为空
+ if hasattr(conversation_info, "knowledge_list") and conversation_info.knowledge_list:
+ # 最多只显示最近的 5 条知识
+ recent_knowledge = conversation_info.knowledge_list[-5:]
+ for i, knowledge_item in enumerate(recent_knowledge):
+ if isinstance(knowledge_item, dict):
+ query = knowledge_item.get("query", "未知查询")
+ knowledge = knowledge_item.get("knowledge", "无知识内容")
+ source = knowledge_item.get("source", "未知来源")
+ # 只取知识内容的前 2000 个字
+ knowledge_snippet = knowledge[:2000] + "..." if len(knowledge) > 2000 else knowledge
+ knowledge_info_str += (
+ f"{i + 1}. 关于 '{query}' (来源: {source}): {knowledge_snippet}\n" # 格式微调,更简洁
+ )
+ else:
+ knowledge_info_str += f"{i + 1}. 发现一条格式不正确的知识记录。\n"
+
+ if not recent_knowledge:
+ knowledge_info_str += "- 暂无。\n" # 更简洁的提示
+
+ else:
+ knowledge_info_str += "- 暂无。\n"
+ except AttributeError:
+ logger.warning(f"[私聊][{self.private_name}]ConversationInfo 对象可能缺少 knowledge_list 属性。")
+ knowledge_info_str += "- 获取知识列表时出错。\n"
+ except Exception as e:
+ logger.error(f"[私聊][{self.private_name}]构建知识信息字符串时出错: {e}")
+ knowledge_info_str += "- 处理知识列表时出错。\n"
+
# 获取聊天历史记录 (chat_history_text)
chat_history_text = observation_info.chat_history_str
if observation_info.new_messages_count > 0 and observation_info.unprocessed_messages:
@@ -162,7 +200,10 @@ class ReplyGenerator:
# --- 格式化最终的 Prompt ---
prompt = prompt_template.format(
- persona_text=persona_text, goals_str=goals_str, chat_history_text=chat_history_text
+ persona_text=persona_text,
+ goals_str=goals_str,
+ chat_history_text=chat_history_text,
+ knowledge_info_str=knowledge_info_str,
)
# --- 调用 LLM 生成 ---
diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py
index f89b3898f..8051d0a8e 100644
--- a/src/plugins/chat/bot.py
+++ b/src/plugins/chat/bot.py
@@ -99,15 +99,20 @@ class ChatBot:
template_group_name = None
async def preprocess():
+ logger.trace("开始预处理消息...")
# 如果在私聊中
if groupinfo is None:
+ logger.trace("检测到私聊消息")
# 是否在配置信息中开启私聊模式
if global_config.enable_friend_chat:
+ logger.trace("私聊模式已启用")
# 是否进入PFC
if global_config.enable_pfc_chatting:
+ logger.trace("进入PFC私聊处理流程")
userinfo = message.message_info.user_info
messageinfo = message.message_info
# 创建聊天流
+ logger.trace(f"为{userinfo.user_id}创建/获取聊天流")
chat = await chat_manager.get_or_create_stream(
platform=messageinfo.platform,
user_info=userinfo,
@@ -118,9 +123,11 @@ class ChatBot:
await self._create_pfc_chat(message)
# 禁止PFC,进入普通的心流消息处理逻辑
else:
+ logger.trace("进入普通心流私聊处理")
await self.heartflow_processor.process_message(message_data)
# 群聊默认进入心流消息处理逻辑
else:
+ logger.trace(f"检测到群聊消息,群ID: {groupinfo.group_id}")
await self.heartflow_processor.process_message(message_data)
if template_group_name:
diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py
index 0a2199f38..493397bbd 100644
--- a/src/plugins/chat/message_sender.py
+++ b/src/plugins/chat/message_sender.py
@@ -159,16 +159,16 @@ class MessageManager:
logger.warning("Processor task already running.")
return
self._processor_task = asyncio.create_task(self._start_processor_loop())
- logger.info("MessageManager processor task started.")
+ logger.debug("MessageManager processor task started.")
def stop(self):
"""停止后台处理器任务。"""
self._running = False
if hasattr(self, "_processor_task") and not self._processor_task.done():
self._processor_task.cancel()
- logger.info("MessageManager processor task stopping.")
+ logger.debug("MessageManager processor task stopping.")
else:
- logger.info("MessageManager processor task not running or already stopped.")
+ logger.debug("MessageManager processor task not running or already stopped.")
async def get_container(self, chat_id: str) -> MessageContainer:
"""获取或创建聊天流的消息容器 (异步,使用锁)"""
diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py
index 4eb3d93dd..71980f484 100644
--- a/src/plugins/chat/utils.py
+++ b/src/plugins/chat/utils.py
@@ -732,6 +732,9 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal"
return f"{int(diff / 86400)}天前:\n"
else:
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)) + ":\n"
+ elif mode == "lite":
+ # 只返回时分秒格式,喵~
+ return time.strftime("%H:%M:%S", time.localtime(timestamp))
return None
diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py
index 7053e3844..f567c5277 100644
--- a/src/plugins/chat/utils_image.py
+++ b/src/plugins/chat/utils_image.py
@@ -5,6 +5,7 @@ import hashlib
from typing import Optional
from PIL import Image
import io
+import numpy as np
from ...common.database import db
@@ -231,14 +232,16 @@ class ImageManager:
return "[图片]"
@staticmethod
- def transform_gif(gif_base64: str) -> str:
- """将GIF转换为水平拼接的静态图像
+ def transform_gif(gif_base64: str, similarity_threshold: float = 1000.0, max_frames: int = 15) -> Optional[str]:
+ """将GIF转换为水平拼接的静态图像, 跳过相似的帧
Args:
gif_base64: GIF的base64编码字符串
+ similarity_threshold: 判定帧相似的阈值 (MSE),越小表示要求差异越大才算不同帧,默认1000.0
+ max_frames: 最大抽取的帧数,默认15
Returns:
- str: 拼接后的JPG图像的base64编码字符串
+ Optional[str]: 拼接后的JPG图像的base64编码字符串, 或者在失败时返回None
"""
try:
# 解码base64
@@ -246,41 +249,88 @@ class ImageManager:
gif = Image.open(io.BytesIO(gif_data))
# 收集所有帧
- frames = []
+ all_frames = []
try:
while True:
- gif.seek(len(frames))
+ gif.seek(len(all_frames))
+ # 确保是RGB格式方便比较
frame = gif.convert("RGB")
- frames.append(frame.copy())
+ all_frames.append(frame.copy())
except EOFError:
- pass
+ pass # 读完啦
- if not frames:
- raise ValueError("No frames found in GIF")
+ if not all_frames:
+ logger.warning("GIF中没有找到任何帧")
+ return None # 空的GIF直接返回None
- # 计算需要抽取的帧的索引
- total_frames = len(frames)
- if total_frames <= 15:
- selected_frames = frames
- else:
- # 均匀抽取10帧
- indices = [int(i * (total_frames - 1) / 14) for i in range(15)]
- selected_frames = [frames[i] for i in indices]
+ # --- 新的帧选择逻辑 ---
+ selected_frames = []
+ last_selected_frame_np = None
- # 获取单帧的尺寸
+ for i, current_frame in enumerate(all_frames):
+ current_frame_np = np.array(current_frame)
+
+ # 第一帧总是要选的
+ if i == 0:
+ selected_frames.append(current_frame)
+ last_selected_frame_np = current_frame_np
+ continue
+
+ # 计算和上一张选中帧的差异(均方误差 MSE)
+ if last_selected_frame_np is not None:
+ mse = np.mean((current_frame_np - last_selected_frame_np) ** 2)
+ # logger.trace(f"帧 {i} 与上一选中帧的 MSE: {mse}") # 可以取消注释来看差异值
+
+ # 如果差异够大,就选它!
+ if mse > similarity_threshold:
+ selected_frames.append(current_frame)
+ last_selected_frame_np = current_frame_np
+ # 检查是不是选够了
+ if len(selected_frames) >= max_frames:
+ # logger.debug(f"已选够 {max_frames} 帧,停止选择。")
+ break
+ # 如果差异不大就跳过这一帧啦
+
+ # --- 帧选择逻辑结束 ---
+
+ # 如果选择后连一帧都没有(比如GIF只有一帧且后续处理失败?)或者原始GIF就没帧,也返回None
+ if not selected_frames:
+ logger.warning("处理后没有选中任何帧")
+ return None
+
+ # logger.debug(f"总帧数: {len(all_frames)}, 选中帧数: {len(selected_frames)}")
+
+ # 获取选中的第一帧的尺寸(假设所有帧尺寸一致)
frame_width, frame_height = selected_frames[0].size
# 计算目标尺寸,保持宽高比
target_height = 200 # 固定高度
+ # 防止除以零
+ if frame_height == 0:
+ logger.error("帧高度为0,无法计算缩放尺寸")
+ return None
target_width = int((target_height / frame_height) * frame_width)
+ # 宽度也不能是0
+ if target_width == 0:
+ logger.warning(f"计算出的目标宽度为0 (原始尺寸 {frame_width}x{frame_height}),调整为1")
+ target_width = 1
- # 调整所有帧的大小
+ # 调整所有选中帧的大小
resized_frames = [
frame.resize((target_width, target_height), Image.Resampling.LANCZOS) for frame in selected_frames
]
# 创建拼接图像
total_width = target_width * len(resized_frames)
+ # 防止总宽度为0
+ if total_width == 0 and len(resized_frames) > 0:
+ logger.warning("计算出的总宽度为0,但有选中帧,可能目标宽度太小")
+ # 至少给点宽度吧
+ total_width = len(resized_frames)
+ elif total_width == 0:
+ logger.error("计算出的总宽度为0且无选中帧")
+ return None
+
combined_image = Image.new("RGB", (total_width, target_height))
# 水平拼接图像
@@ -289,14 +339,17 @@ class ImageManager:
# 转换为base64
buffer = io.BytesIO()
- combined_image.save(buffer, format="JPEG", quality=85)
+ combined_image.save(buffer, format="JPEG", quality=85) # 保存为JPEG
result_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
return result_base64
+ except MemoryError:
+ logger.error("GIF转换失败: 内存不足,可能是GIF太大或帧数太多")
+ return None # 内存不够啦
except Exception as e:
- logger.error(f"GIF转换失败: {str(e)}")
- return None
+ logger.error(f"GIF转换失败: {str(e)}", exc_info=True) # 记录详细错误信息
+ return None # 其他错误也返回None
# 创建全局单例
diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py
index bcabf104e..d6da4ce3f 100644
--- a/src/plugins/emoji_system/emoji_manager.py
+++ b/src/plugins/emoji_system/emoji_manager.py
@@ -34,9 +34,12 @@ MAX_EMOJI_FOR_PROMPT = 20 # 最大允许的表情包描述数量于图片替换
class MaiEmoji:
"""定义一个表情包"""
- def __init__(self, filename: str, path: str):
- self.path = path # 存储目录路径
- self.filename = filename
+ def __init__(self, full_path: str):
+ if not full_path:
+ raise ValueError("full_path cannot be empty")
+ self.full_path = full_path # 文件的完整路径 (包括文件名)
+ self.path = os.path.dirname(full_path) # 文件所在的目录路径
+ self.filename = os.path.basename(full_path) # 文件名
self.embedding = []
self.hash = "" # 初始为空,在创建实例时会计算
self.description = ""
@@ -48,35 +51,58 @@ class MaiEmoji:
self.format = ""
async def initialize_hash_format(self):
- """从文件创建表情包实例
-
- 参数:
- file_path: 文件的完整路径
-
- 返回:
- MaiEmoji: 创建的表情包实例,如果失败则返回None
- """
+ """从文件创建表情包实例, 计算哈希值和格式"""
+ image_base64 = None
+ image_bytes = None
try:
- file_path = os.path.join(self.path, self.filename)
- if not os.path.exists(file_path):
- logger.error(f"[错误] 表情包文件不存在: {file_path}")
+ # 使用 full_path 检查文件是否存在
+ if not os.path.exists(self.full_path):
+ logger.error(f"[初始化错误] 表情包文件不存在: {self.full_path}")
+ self.is_deleted = True
return None
- image_base64 = image_path_to_base64(file_path)
+ # 使用 full_path 读取文件
+ logger.debug(f"[初始化] 正在读取文件: {self.full_path}")
+ image_base64 = image_path_to_base64(self.full_path)
if image_base64 is None:
- logger.error(f"[错误] 无法读取图片: {file_path}")
+ logger.error(f"[初始化错误] 无法读取或转换Base64: {self.full_path}")
+ self.is_deleted = True
return None
+ logger.debug(f"[初始化] 文件读取成功 (Base64预览: {image_base64[:50]}...)")
# 计算哈希值
+ logger.debug(f"[初始化] 正在解码Base64并计算哈希: {self.filename}")
image_bytes = base64.b64decode(image_base64)
self.hash = hashlib.md5(image_bytes).hexdigest()
+ logger.debug(f"[初始化] 哈希计算成功: {self.hash}")
# 获取图片格式
- self.format = Image.open(io.BytesIO(image_bytes)).format.lower()
+ logger.debug(f"[初始化] 正在使用Pillow获取格式: {self.filename}")
+ try:
+ with Image.open(io.BytesIO(image_bytes)) as img:
+ self.format = img.format.lower()
+ logger.debug(f"[初始化] 格式获取成功: {self.format}")
+ except Exception as pil_error:
+ logger.error(f"[初始化错误] Pillow无法处理图片 ({self.filename}): {pil_error}")
+ logger.error(traceback.format_exc())
+ self.is_deleted = True
+ return None
+ # 如果所有步骤成功,返回 True
+ return True
+
+ except FileNotFoundError:
+ logger.error(f"[初始化错误] 文件在处理过程中丢失: {self.full_path}")
+ self.is_deleted = True
+ return None
+ except base64.binascii.Error as b64_error:
+ logger.error(f"[初始化错误] Base64解码失败 ({self.filename}): {b64_error}")
+ self.is_deleted = True
+ return None
except Exception as e:
- logger.error(f"[错误] 初始化表情包失败: {str(e)}")
+ logger.error(f"[初始化错误] 初始化表情包时发生未预期错误 ({self.filename}): {str(e)}")
logger.error(traceback.format_exc())
+ self.is_deleted = True
return None
async def register_to_db(self):
@@ -87,61 +113,72 @@ class MaiEmoji:
"""
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)
+ # 源路径是当前实例的完整路径 self.full_path
+ source_full_path = self.full_path
+ # 目标完整路径
+ destination_full_path = os.path.join(EMOJI_REGISTED_DIR, self.filename)
# 检查源文件是否存在
- if not os.path.exists(source_path):
- logger.error(f"[错误] 源文件不存在: {source_path}")
+ if not os.path.exists(source_full_path):
+ logger.error(f"[错误] 源文件不存在: {source_full_path}")
return False
# --- 文件移动 ---
try:
# 如果目标文件已存在,先删除 (确保移动成功)
- if os.path.exists(destination_path):
- os.remove(destination_path)
+ if os.path.exists(destination_full_path):
+ os.remove(destination_full_path)
- os.rename(source_path, destination_path)
- logger.info(f"[移动] 文件从 {source_path} 移动到 {destination_path}")
- # 更新实例的路径属性为新目录
+ os.rename(source_full_path, destination_full_path)
+ logger.debug(f"[移动] 文件从 {source_full_path} 移动到 {destination_full_path}")
+ # 更新实例的路径属性为新路径
+ self.full_path = destination_full_path
self.path = EMOJI_REGISTED_DIR
+ # self.filename 保持不变
except Exception as move_error:
logger.error(f"[错误] 移动文件失败: {str(move_error)}")
- return False # 文件移动失败,不继续
+ # 如果移动失败,尝试将实例状态恢复?暂时不处理,仅返回失败
+ return False
# --- 数据库操作 ---
try:
# 准备数据库记录 for emoji collection
emoji_record = {
"filename": self.filename,
- "path": os.path.join(self.path, self.filename), # 使用更新后的路径
+ "path": self.path, # 存储目录路径
+ "full_path": self.full_path, # 存储完整文件路径
"embedding": self.embedding,
"description": self.description,
- "emotion": self.emotion, # 添加情感标签字段
+ "emotion": self.emotion,
"hash": self.hash,
"format": self.format,
- "timestamp": int(self.register_time), # 使用实例的注册时间
+ "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}")
+
+ logger.success(f"[注册] 表情包信息保存到数据库: {self.filename} ({self.emotion})")
return True
except Exception as db_error:
- logger.error(f"[错误] 保存数据库失败: {str(db_error)}")
- # 考虑是否需要将文件移回?为了简化,暂时只记录错误
+ logger.error(f"[错误] 保存数据库失败 ({self.filename}): {str(db_error)}")
+ # 数据库保存失败,是否需要将文件移回?为了简化,暂时只记录错误
+ # 可以考虑在这里尝试删除已移动的文件,避免残留
+ try:
+ if os.path.exists(self.full_path): # full_path 此时是目标路径
+ os.remove(self.full_path)
+ logger.warning(f"[回滚] 已删除移动失败后残留的文件: {self.full_path}")
+ except Exception as remove_error:
+ logger.error(f"[错误] 回滚删除文件失败: {remove_error}")
return False
except Exception as e:
- logger.error(f"[错误] 注册表情包失败: {str(e)}")
+ logger.error(f"[错误] 注册表情包失败 ({self.filename}): {str(e)}")
logger.error(traceback.format_exc())
return False
@@ -155,30 +192,36 @@ class MaiEmoji:
"""
try:
# 1. 删除文件
- if os.path.exists(os.path.join(self.path, self.filename)):
+ file_to_delete = self.full_path
+ if os.path.exists(file_to_delete):
try:
- os.remove(os.path.join(self.path, self.filename))
- logger.info(f"[删除] 文件: {os.path.join(self.path, self.filename)}")
+ os.remove(file_to_delete)
+ logger.debug(f"[删除] 文件: {file_to_delete}")
except Exception as e:
- logger.error(f"[错误] 删除文件失败 {os.path.join(self.path, self.filename)}: {str(e)}")
- # 继续执行,即使文件删除失败也尝试删除数据库记录
+ logger.error(f"[错误] 删除文件失败 {file_to_delete}: {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}")
-
+ logger.info(f"[删除] 表情包数据库记录 {self.filename} (Hash: {self.hash})")
# 3. 标记对象已被删除
self.is_deleted = True
return True
else:
- logger.error(f"[错误] 删除表情包记录失败: {self.hash}")
+ # 如果数据库记录删除失败,但文件可能已删除,记录一个警告
+ if not os.path.exists(file_to_delete):
+ logger.warning(
+ f"[警告] 表情包文件 {file_to_delete} 已删除,但数据库记录删除失败 (Hash: {self.hash})"
+ )
+ else:
+ logger.error(f"[错误] 删除表情包数据库记录失败: {self.hash}")
return False
except Exception as e:
- logger.error(f"[错误] 删除表情包失败: {str(e)}")
+ logger.error(f"[错误] 删除表情包失败 ({self.filename}): {str(e)}")
return False
@@ -195,7 +238,7 @@ class EmojiManager:
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"
+ model=global_config.llm_normal, max_tokens=600, request_type="emoji"
) # 更高的温度,更少的token(后续可以根据情绪来调整温度)
self.emoji_num = 0
@@ -208,6 +251,7 @@ class EmojiManager:
def _ensure_emoji_dir(self):
"""确保表情存储目录存在"""
os.makedirs(EMOJI_DIR, exist_ok=True)
+ os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True)
def initialize(self):
"""初始化数据库连接和表情目录"""
@@ -264,59 +308,71 @@ class EmojiManager:
Args:
text_emotion: 输入的情感描述文本
Returns:
- Optional[Tuple[str, str]]: (表情包文件路径, 表情包描述),如果没有找到则返回None
+ Optional[Tuple[str, str]]: (表情包完整文件路径, 表情包描述),如果没有找到则返回None
"""
try:
self._ensure_db()
- time_start = time.time()
+ _time_start = time.time()
- # 获取所有表情包
+ # 获取所有表情包 (从内存缓存中获取)
all_emojis = self.emoji_objects
if not all_emojis:
- logger.warning("数据库中没有任何表情包")
+ logger.warning("内存中没有任何表情包对象")
+ # 可以考虑再查一次数据库?或者依赖定期任务更新
return None
# 计算每个表情包与输入文本的最大情感相似度
emoji_similarities = []
for emoji in all_emojis:
+ # 跳过已标记为删除的对象
+ if emoji.is_deleted:
+ continue
+
emotions = emoji.emotion
if not emotions:
continue
# 计算与每个emotion标签的相似度,取最大值
max_similarity = 0
+ best_matching_emotion = "" # 记录最匹配的 emotion 喵~
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)
+ if similarity > max_similarity: # 如果找到更相似的喵~
+ max_similarity = similarity
+ best_matching_emotion = emotion # 就记下这个 emotion 喵~
- emoji_similarities.append((emoji, max_similarity))
+ if best_matching_emotion: # 确保有匹配的情感才添加喵~
+ emoji_similarities.append((emoji, max_similarity, best_matching_emotion)) # 把 emotion 也存起来喵~
# 按相似度降序排序
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
+ # 获取前10个最相似的表情包
+ top_emojis = (
+ emoji_similarities[:10] if len(emoji_similarities) > 10 else emoji_similarities
+ ) # 改个名字,更清晰喵~
- if not top_5_emojis:
+ if not top_emojis:
logger.warning("未找到匹配的表情包")
return None
- # 从前5个中随机选择一个
- selected_emoji, similarity = random.choice(top_5_emojis)
+ # 从前几个中随机选择一个
+ selected_emoji, similarity, matched_emotion = random.choice(top_emojis) # 把匹配的 emotion 也拿出来喵~
# 更新使用次数
self.record_usage(selected_emoji.hash)
- time_end = time.time()
+ _time_end = time.time()
- logger.info(
- f"找到[{text_emotion}]表情包,用时:{time_end - time_start:.2f}秒: {selected_emoji.description} (相似度: {similarity:.4f})"
+ logger.info( # 使用匹配到的 emotion 记录日志喵~
+ f"为[{text_emotion}]找到表情包: {matched_emotion} ({selected_emoji.filename}), Similarity: {similarity:.4f}"
)
- return selected_emoji.path, f"[ {selected_emoji.description} ]"
+ # 返回完整文件路径和描述
+ return selected_emoji.full_path, f"[ {selected_emoji.description} ]"
except Exception as e:
logger.error(f"[错误] 获取表情包失败: {str(e)}")
@@ -364,40 +420,50 @@ class EmojiManager:
self.emoji_num = total_count
removed_count = 0
# 使用列表复制进行遍历,因为我们会在遍历过程中修改列表
- for emoji in self.emoji_objects[:]:
+ objects_to_remove = []
+ for emoji in self.emoji_objects:
try:
+ # 跳过已经标记为删除的,避免重复处理
+ if emoji.is_deleted:
+ objects_to_remove.append(emoji) # 收集起来一次性移除
+ continue
+
# 检查文件是否存在
- if not os.path.exists(emoji.path):
- logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}")
+ if not os.path.exists(emoji.full_path):
+ logger.warning(f"[检查] 表情包文件丢失: {emoji.full_path}")
# 执行表情包对象的删除方法
- await emoji.delete()
- # 从列表中移除该对象
- self.emoji_objects.remove(emoji)
+ await emoji.delete() # delete 方法现在会标记 is_deleted
+ objects_to_remove.append(emoji) # 标记删除后,也收集起来移除
# 更新计数
self.emoji_num -= 1
removed_count += 1
continue
- if emoji.description == None:
- logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}")
- # 执行表情包对象的删除方法
+ # 检查描述是否为空 (如果为空也视为无效)
+ if not emoji.description:
+ logger.warning(f"[检查] 表情包描述为空,视为无效: {emoji.filename}")
await emoji.delete()
- # 从列表中移除该对象
- self.emoji_objects.remove(emoji)
- # 更新计数
+ objects_to_remove.append(emoji)
self.emoji_num -= 1
removed_count += 1
continue
except Exception as item_error:
- logger.error(f"[错误] 处理表情包记录时出错: {str(item_error)}")
+ logger.error(f"[错误] 处理表情包记录时出错 ({emoji.filename}): {str(item_error)}")
+ # 即使出错,也尝试继续检查下一个
continue
+ # 从 self.emoji_objects 中移除标记的对象
+ if objects_to_remove:
+ self.emoji_objects = [e for e in self.emoji_objects if e not in objects_to_remove]
+
+ # 清理 EMOJI_REGISTED_DIR 目录中未被追踪的文件
await self.clean_unused_emojis(EMOJI_REGISTED_DIR, self.emoji_objects)
+
# 输出清理结果
if removed_count > 0:
- logger.success(f"[清理] 已清理 {removed_count} 个失效的表情包记录")
- logger.info(f"[统计] 清理前: {total_count} | 清理后: {len(self.emoji_objects)}")
+ logger.success(f"[清理] 已清理 {removed_count} 个失效/文件丢失的表情包记录")
+ logger.info(f"[统计] 清理前记录数: {total_count} | 清理后有效记录数: {len(self.emoji_objects)}")
else:
logger.info(f"[检查] 已检查 {total_count} 个表情包记录,全部完好")
@@ -460,45 +526,72 @@ class EmojiManager:
await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60)
async def get_all_emoji_from_db(self):
- """获取所有表情包并初始化为MaiEmoji类对象
-
- 参数:
- hash: 可选,如果提供则只返回指定哈希值的表情包
-
- 返回:
- list[MaiEmoji]: 表情包对象列表
- """
+ """获取所有表情包并初始化为MaiEmoji类对象,更新 self.emoji_objects"""
try:
self._ensure_db()
+ logger.info("[数据库] 开始加载所有表情包记录...")
- # 获取所有表情包
all_emoji_data = list(db.emoji.find())
-
- # 将数据库记录转换为MaiEmoji对象
emoji_objects = []
+ load_errors = 0
+
for emoji_data in all_emoji_data:
- emoji = MaiEmoji(
- filename=emoji_data.get("filename", ""),
- path=emoji_data.get("path", ""),
- )
+ full_path = emoji_data.get("full_path")
+ if not full_path:
+ logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}")
+ load_errors += 1
+ continue # 跳过缺少 full_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)
+ try:
+ # 使用 full_path 初始化 MaiEmoji 对象
+ emoji = MaiEmoji(full_path=full_path)
- # 存储到EmojiManager中
+ # 设置从数据库加载的属性
+ emoji.hash = emoji_data.get("hash", "")
+ # 如果 hash 为空,也跳过?取决于业务逻辑
+ if not emoji.hash:
+ logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}")
+ load_errors += 1
+ continue
+
+ emoji.description = emoji_data.get("description", "")
+ emoji.emotion = emoji_data.get("emotion", [])
+ emoji.usage_count = emoji_data.get("usage_count", 0)
+ # 优先使用 last_used_time,否则用 timestamp,最后用当前时间
+ last_used = emoji_data.get("last_used_time")
+ timestamp = emoji_data.get("timestamp")
+ emoji.last_used_time = (
+ last_used if last_used is not None else (timestamp if timestamp is not None else time.time())
+ )
+ emoji.register_time = timestamp if timestamp is not None else time.time()
+ emoji.format = emoji_data.get("format", "") # 加载格式
+
+ # 不需要再手动设置 path 和 filename,__init__ 会自动处理
+
+ emoji_objects.append(emoji)
+
+ except ValueError as ve: # 捕获 __init__ 可能的错误
+ logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}")
+ load_errors += 1
+ except Exception as e:
+ logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}")
+ load_errors += 1
+
+ # 更新内存中的列表和数量
self.emoji_objects = emoji_objects
+ self.emoji_num = len(emoji_objects)
+
+ logger.success(f"[数据库] 加载完成: 共加载 {self.emoji_num} 个表情包记录。")
+ if load_errors > 0:
+ logger.warning(f"[数据库] 加载过程中出现 {load_errors} 个错误。")
except Exception as e:
- logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}")
+ logger.error(f"[错误] 从数据库加载所有表情包对象失败: {str(e)}")
+ self.emoji_objects = [] # 加载失败则清空列表
+ self.emoji_num = 0
async def get_emoji_from_db(self, hash=None):
- """获取所有表情包并初始化为MaiEmoji类对象
+ """获取指定哈希值的表情包并初始化为MaiEmoji类对象列表 (主要用于调试或特定查找)
参数:
hash: 可选,如果提供则只返回指定哈希值的表情包
@@ -509,50 +602,73 @@ class EmojiManager:
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", ""),
+ else:
+ logger.warning(
+ "[查询] 未提供 hash,将尝试加载所有表情包,建议使用 get_all_emoji_from_db 更新管理器状态。"
)
- # 设置额外属性
- 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_data_list = list(db.emoji.find(query))
+ emoji_objects = []
+ load_errors = 0
- emoji_objects.append(emoji)
+ for emoji_data in emoji_data_list:
+ full_path = emoji_data.get("full_path")
+ if not full_path:
+ logger.warning(f"[加载错误] 数据库记录缺少 'full_path' 字段: {emoji_data.get('_id')}")
+ load_errors += 1
+ continue
- # 存储到EmojiManager中
- self.emoji_objects = emoji_objects
+ try:
+ emoji = MaiEmoji(full_path=full_path)
+ emoji.hash = emoji_data.get("hash", "")
+ if not emoji.hash:
+ logger.warning(f"[加载错误] 数据库记录缺少 'hash' 字段: {full_path}")
+ load_errors += 1
+ continue
+
+ emoji.description = emoji_data.get("description", "")
+ emoji.emotion = emoji_data.get("emotion", [])
+ emoji.usage_count = emoji_data.get("usage_count", 0)
+ last_used = emoji_data.get("last_used_time")
+ timestamp = emoji_data.get("timestamp")
+ emoji.last_used_time = (
+ last_used if last_used is not None else (timestamp if timestamp is not None else time.time())
+ )
+ emoji.register_time = timestamp if timestamp is not None else time.time()
+ emoji.format = emoji_data.get("format", "")
+ emoji_objects.append(emoji)
+ except ValueError as ve:
+ logger.error(f"[加载错误] 初始化 MaiEmoji 失败 ({full_path}): {ve}")
+ load_errors += 1
+ except Exception as e:
+ logger.error(f"[加载错误] 处理数据库记录时出错 ({full_path}): {str(e)}")
+ load_errors += 1
+
+ if load_errors > 0:
+ logger.warning(f"[查询] 加载过程中出现 {load_errors} 个错误。")
return emoji_objects
except Exception as e:
- logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}")
+ logger.error(f"[错误] 从数据库获取表情包对象失败: {str(e)}")
return []
- async def get_emoji_from_manager(self, hash) -> MaiEmoji:
- """从EmojiManager中获取表情包
+ async def get_emoji_from_manager(self, hash) -> Optional[MaiEmoji]:
+ """从内存中的 emoji_objects 列表获取表情包
参数:
- hash:如果提供则只返回指定哈希值的表情包
+ hash: 要查找的表情包哈希值
+ 返回:
+ MaiEmoji 或 None: 如果找到则返回 MaiEmoji 对象,否则返回 None
"""
for emoji in self.emoji_objects:
- if emoji.hash == hash:
+ # 确保对象未被标记为删除且哈希值匹配
+ if not emoji.is_deleted and emoji.hash == hash:
return emoji
- return None
+ return None # 如果循环结束还没找到,则返回 None
async def delete_emoji(self, emoji_hash: str) -> bool:
"""根据哈希值删除表情包
@@ -656,11 +772,11 @@ class EmojiManager:
# 调用大模型进行决策
decision, _ = await self.llm_emotion_judge.generate_response_async(prompt, temperature=0.8)
- logger.info(f"[决策] 大模型决策结果: {decision}")
+ logger.info(f"[决策] 结果: {decision}")
# 解析决策结果
if "不删除" in decision:
- logger.info("[决策] 决定不删除任何表情包")
+ logger.info("[决策] 不删除任何表情包")
return False
# 尝试从决策中提取表情包编号
@@ -673,7 +789,7 @@ class EmojiManager:
emoji_to_delete = selected_emojis[emoji_index]
# 删除选定的表情包
- logger.info(f"[决策] 决定删除表情包: {emoji_to_delete.description}")
+ logger.info(f"[决策] 删除表情包: {emoji_to_delete.description}")
delete_success = await self.delete_emoji(emoji_to_delete.hash)
if delete_success:
@@ -682,7 +798,7 @@ class EmojiManager:
if register_success:
self.emoji_objects.append(new_emoji)
self.emoji_num += 1
- logger.success(f"[成功] 注册表情包: {new_emoji.description}")
+ logger.success(f"[成功] 注册: {new_emoji.filename}")
return True
else:
logger.error(f"[错误] 注册表情包到数据库失败: {new_emoji.filename}")
@@ -719,10 +835,10 @@ class EmojiManager:
# 调用AI获取描述
if image_format == "gif" or image_format == "GIF":
image_base64 = image_manager.transform_gif(image_base64)
- prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,详细描述一下表情包表达的情感和内容,请关注其幽默和讽刺意味"
+ prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,描述一下表情包表达的情感和内容,描述细节,从互联网梗,meme的角度去分析"
description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, "jpg")
else:
- prompt = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,请关注其幽默和讽刺意味"
+ prompt = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,描述细节,从互联网梗,meme的角度去分析"
description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format)
# 审核表情包
@@ -741,17 +857,22 @@ class EmojiManager:
# 分析情感含义
emotion_prompt = f"""
- 基于这个表情包的描述:'{description}',请列出1-2个可能的情感标签,每个标签用一个词组表示,格式如下:
- 幽默的讽刺
- 悲伤的无奈
- 愤怒的抗议
- 愤怒的讽刺
- 直接输出词组,词组检用逗号分隔。"""
+ 请你识别这个表情包的含义和适用场景,给我简短的描述,每个描述不要超过15个字
+ 这是一个基于这个表情包的描述:'{description}'
+ 你可以关注其幽默和讽刺意味,动用贴吧,微博,小红书的知识,必须从互联网梗,meme的角度去分析
+ 请直接输出描述,不要出现任何其他内容,如果有多个描述,可以用逗号分隔
+ """
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()]
+ # 根据情感标签数量随机选择喵~超过5个选3个,超过2个选2个
+ if len(emotions) > 5:
+ emotions = random.sample(emotions, 3)
+ elif len(emotions) > 2:
+ emotions = random.sample(emotions, 2)
+
return f"[表情包:{description}]", emotions
except Exception as e:
@@ -767,100 +888,176 @@ class EmojiManager:
Returns:
bool: 注册是否成功
"""
+ file_full_path = os.path.join(EMOJI_DIR, filename)
+ if not os.path.exists(file_full_path):
+ logger.error(f"[注册失败] 文件不存在: {file_full_path}")
+ return False
+
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 == "" or description == None:
+ # 1. 创建 MaiEmoji 实例并初始化哈希和格式
+ new_emoji = MaiEmoji(full_path=file_full_path)
+ init_result = await new_emoji.initialize_hash_format()
+ if init_result is None or new_emoji.is_deleted: # 初始化失败或文件读取错误
+ logger.error(f"[注册失败] 初始化哈希和格式失败: {filename}")
+ # 是否需要删除源文件?看业务需求,暂时不删
return False
- new_emoji.description = description
- new_emoji.emotion = emotions
- # 检查是否已经注册过
- # 对比内存中是否存在相同哈希值的表情包
+ # 2. 检查哈希是否已存在 (在内存中检查)
if await self.get_emoji_from_manager(new_emoji.hash):
- logger.warning(f"[警告] 表情包已存在: {filename}")
+ logger.warning(f"[注册跳过] 表情包已存在 (Hash: {new_emoji.hash}): {filename}")
+ # 删除重复的源文件
+ try:
+ os.remove(file_full_path)
+ logger.info(f"[清理] 删除重复的待注册文件: {filename}")
+ except Exception as e:
+ logger.error(f"[错误] 删除重复文件失败: {str(e)}")
+ return False # 返回 False 表示未注册新表情
+
+ # 3. 构建描述和情感
+ try:
+ emoji_base64 = image_path_to_base64(file_full_path)
+ if emoji_base64 is None: # 再次检查读取
+ logger.error(f"[注册失败] 无法读取图片以生成描述: {filename}")
+ return False
+ description, emotions = await self.build_emoji_description(emoji_base64)
+ if not description: # 检查描述是否成功生成或审核通过
+ logger.warning(f"[注册失败] 未能生成有效描述或审核未通过: {filename}")
+ # 删除未能生成描述的文件
+ try:
+ os.remove(file_full_path)
+ logger.info(f"[清理] 删除描述生成失败的文件: {filename}")
+ except Exception as e:
+ logger.error(f"[错误] 删除描述生成失败文件时出错: {str(e)}")
+ return False
+ new_emoji.description = description
+ new_emoji.emotion = emotions
+ except Exception as build_desc_error:
+ logger.error(f"[注册失败] 生成描述/情感时出错 ({filename}): {build_desc_error}")
+ # 同样考虑删除文件
+ try:
+ os.remove(file_full_path)
+ logger.info(f"[清理] 删除描述生成异常的文件: {filename}")
+ except Exception as e:
+ logger.error(f"[错误] 删除描述生成异常文件时出错: {str(e)}")
return False
+ # 4. 检查容量并决定是否替换或直接注册
if self.emoji_num >= self.emoji_num_max:
- logger.warning(f"表情包数量已达到上限({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("[错误] 替换表情包失败,无法完成注册")
+ logger.error("[注册失败] 替换表情包失败,无法完成注册")
+ # 替换失败,删除新表情包文件
+ try:
+ os.remove(file_full_path) # new_emoji 的 full_path 此时还是源路径
+ logger.info(f"[清理] 删除替换失败的新表情文件: {filename}")
+ except Exception as e:
+ logger.error(f"[错误] 删除替换失败文件时出错: {str(e)}")
return False
+ # 替换成功时,replace_a_emoji 内部已处理 new_emoji 的注册和添加到列表
return True
else:
- # 修复:等待异步注册完成
- register_success = await new_emoji.register_to_db()
+ # 直接注册
+ register_success = await new_emoji.register_to_db() # 此方法会移动文件并更新 DB
if register_success:
+ # 注册成功后,添加到内存列表
self.emoji_objects.append(new_emoji)
self.emoji_num += 1
- logger.success(f"[成功] 注册表情包: {filename}")
+ logger.success(f"[成功] 注册新表情包: {filename} (当前: {self.emoji_num}/{self.emoji_num_max})")
return True
else:
- logger.error(f"[错误] 注册表情包到数据库失败: {filename}")
+ logger.error(f"[注册失败] 保存表情包到数据库/移动文件失败: {filename}")
+ # register_to_db 失败时,内部会尝试清理移动后的文件,源文件可能还在
+ # 是否需要删除源文件?
+ if os.path.exists(file_full_path):
+ try:
+ os.remove(file_full_path)
+ logger.info(f"[清理] 删除注册失败的源文件: {filename}")
+ except Exception as e:
+ logger.error(f"[错误] 删除注册失败源文件时出错: {str(e)}")
return False
except Exception as e:
- logger.error(f"[错误] 注册表情包失败: {str(e)}")
+ logger.error(f"[错误] 注册表情包时发生未预期错误 ({filename}): {str(e)}")
logger.error(traceback.format_exc())
+ # 尝试删除源文件以避免循环处理
+ if os.path.exists(file_full_path):
+ try:
+ os.remove(file_full_path)
+ logger.info(f"[清理] 删除处理异常的源文件: {filename}")
+ except Exception as remove_error:
+ logger.error(f"[错误] 删除异常处理文件时出错: {remove_error}")
return False
async def clear_temp_emoji(self):
- """每天清理临时表情包
+ """清理临时表情包
清理/data/emoji和/data/image目录下的所有文件
- 当目录中文件数超过50时,会全部删除
+ 当目录中文件数超过100时,会全部删除
"""
- logger.info("[清理] 开始清理临时表情包...")
+ 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:
+ if len(files) > 100:
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}")
+ 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:
+ if len(files) > 100:
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.debug(f"[清理] 删除图片: {filename}")
- logger.success("[清理] 临时文件清理完成")
+ logger.success("[清理] 完成")
async def clean_unused_emojis(self, emoji_dir, emoji_objects):
- """清理未使用的表情包文件
- 遍历指定文件夹中的所有文件,删除未在emoji_objects列表中的文件
- """
- # 获取所有表情包路径
- emoji_paths = {emoji.path for emoji in emoji_objects}
+ """清理指定目录中未被 emoji_objects 追踪的表情包文件"""
+ if not os.path.exists(emoji_dir):
+ logger.warning(f"[清理] 目标目录不存在,跳过清理: {emoji_dir}")
+ return
- # 遍历文件夹中的所有文件
- for file_name in os.listdir(emoji_dir):
- file_path = os.path.join(emoji_dir, file_name)
+ try:
+ # 获取内存中所有有效表情包的完整路径集合
+ tracked_full_paths = {emoji.full_path for emoji in emoji_objects if not emoji.is_deleted}
+ cleaned_count = 0
- # 检查文件是否在表情包路径列表中
- if file_path not in emoji_paths:
- try:
- # 删除未在表情包列表中的文件
- os.remove(file_path)
- logger.info(f"[清理] 删除未使用的表情包文件: {file_path}")
- except Exception as e:
- logger.error(f"[错误] 删除文件时出错: {str(e)}")
+ # 遍历指定目录中的所有文件
+ for file_name in os.listdir(emoji_dir):
+ file_full_path = os.path.join(emoji_dir, file_name)
+
+ # 确保处理的是文件而不是子目录
+ if not os.path.isfile(file_full_path):
+ continue
+
+ # 如果文件不在被追踪的集合中,则删除
+ if file_full_path not in tracked_full_paths:
+ try:
+ os.remove(file_full_path)
+ logger.info(f"[清理] 删除未追踪的表情包文件: {file_full_path}")
+ cleaned_count += 1
+ except Exception as e:
+ logger.error(f"[错误] 删除文件时出错 ({file_full_path}): {str(e)}")
+
+ if cleaned_count > 0:
+ logger.success(f"[清理] 在目录 {emoji_dir} 中清理了 {cleaned_count} 个破损表情包。")
+ else:
+ logger.info(f"[清理] 目录 {emoji_dir} 中没有需要清理的。")
+
+ except Exception as e:
+ logger.error(f"[错误] 清理未使用表情包文件时出错 ({emoji_dir}): {str(e)}")
# 创建全局单例
diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py
index c15c4f83f..73d679e4e 100644
--- a/src/plugins/heartFC_chat/heartFC_chat.py
+++ b/src/plugins/heartFC_chat/heartFC_chat.py
@@ -2,6 +2,7 @@ import asyncio
import time
import traceback
import random # <--- 添加导入
+import json # <--- 确保导入 json
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
from collections import deque
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
@@ -14,9 +15,7 @@ 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_calculator import Timer # <--- Import Timer
-from src.do_tool.tool_use import ToolUser
from src.plugins.emoji_system.emoji_manager import emoji_manager
-from src.plugins.utils.json_utils import process_llm_tool_calls, extract_tool_call_arguments
from src.heart_flow.sub_mind import SubMind
from src.heart_flow.observation import Observation
from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
@@ -30,12 +29,14 @@ from src.plugins.moods.moods import MoodManager
from src.individuality.individuality import Individuality
-INITIAL_DURATION = 60.0
-
WAITING_TIME_THRESHOLD = 300 # 等待新消息时间阈值,单位秒
+EMOJI_SEND_PRO = 0.3 # 设置一个概率,比如 30% 才真的发
-logger = get_logger("interest") # Logger Name Changed
+CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值
+
+
+logger = get_logger("hfc") # Logger Name Changed
# 默认动作定义
@@ -117,35 +118,6 @@ class ActionManager:
"""重置为默认动作集"""
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"],
- },
- },
- }
- ]
-
# 在文件开头添加自定义异常类
class HeartFCError(Exception):
@@ -179,8 +151,6 @@ class HeartFChatting:
其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。
"""
- CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值
-
def __init__(
self,
chat_id: str,
@@ -222,7 +192,6 @@ class HeartFChatting:
max_tokens=256,
request_type="response_heartflow",
)
- self.tool_user = ToolUser()
self.heart_fc_sender = HeartFCSender()
# LLM规划器配置
@@ -261,7 +230,7 @@ class HeartFChatting:
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} ")
+ logger.debug(f"{self.log_prefix}麦麦感觉到了,可以开始认真水群 ")
return True
async def start(self):
@@ -292,7 +261,7 @@ class HeartFChatting:
pass # 忽略取消或超时错误
self._loop_task = None # 清理旧任务引用
- logger.info(f"{self.log_prefix} 启动认真水群(HFC)主循环...")
+ logger.debug(f"{self.log_prefix} 启动认真水群(HFC)主循环...")
# 创建新的循环任务
self._loop_task = asyncio.create_task(self._hfc_loop())
# 添加完成回调
@@ -470,6 +439,16 @@ class HeartFChatting:
# execute:执行
+ # 在此处添加日志记录
+ if action == "text_reply":
+ action_str = "回复"
+ elif action == "emoji_reply":
+ action_str = "回复表情"
+ else:
+ action_str = "不回复"
+
+ logger.info(f"{self.log_prefix} 麦麦决定'{action_str}', 原因'{reasoning}'")
+
return await self._handle_action(
action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time
)
@@ -644,14 +623,14 @@ class HeartFChatting:
self._lian_xu_bu_hui_fu_ci_shu += 1
self._lian_xu_deng_dai_shi_jian += dang_qian_deng_dai # 累加等待时间
logger.debug(
- f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{self.CONSECUTIVE_NO_REPLY_THRESHOLD}, "
+ f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{CONSECUTIVE_NO_REPLY_THRESHOLD}, "
f"本次等待: {dang_qian_deng_dai:.2f}秒, 累计等待: {self._lian_xu_deng_dai_shi_jian:.2f}秒"
)
# 检查是否同时达到次数和时间阈值
- time_threshold = 0.66 * WAITING_TIME_THRESHOLD * self.CONSECUTIVE_NO_REPLY_THRESHOLD
+ time_threshold = 0.66 * WAITING_TIME_THRESHOLD * CONSECUTIVE_NO_REPLY_THRESHOLD
if (
- self._lian_xu_bu_hui_fu_ci_shu >= self.CONSECUTIVE_NO_REPLY_THRESHOLD
+ self._lian_xu_bu_hui_fu_ci_shu >= CONSECUTIVE_NO_REPLY_THRESHOLD
and self._lian_xu_deng_dai_shi_jian >= time_threshold
):
logger.info(
@@ -661,7 +640,7 @@ class HeartFChatting:
)
# 调用回调。注意:这里不重置计数器和时间,依赖回调函数成功改变状态来隐式重置上下文。
await self.on_consecutive_no_reply_callback()
- elif self._lian_xu_bu_hui_fu_ci_shu >= self.CONSECUTIVE_NO_REPLY_THRESHOLD:
+ elif self._lian_xu_bu_hui_fu_ci_shu >= CONSECUTIVE_NO_REPLY_THRESHOLD:
# 仅次数达到阈值,但时间未达到
logger.debug(
f"{self.log_prefix} 连续不回复次数达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次) "
@@ -784,41 +763,36 @@ class HeartFChatting:
async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]:
"""
规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。
+ 重构为:让LLM返回结构化JSON文本,然后在代码中解析。
参数:
current_mind: 子思维的当前思考结果
cycle_timers: 计时器字典
- is_re_planned: 是否为重新规划
+ is_re_planned: 是否为重新规划 (此重构中暂时简化,不处理 is_re_planned 的特殊逻辑)
"""
- logger.info(f"{self.log_prefix}[Planner] 开始{'重新' if is_re_planned else ''}执行规划器")
+ logger.info(f"{self.log_prefix}开始想要做什么")
- # --- 新增:检查历史动作并调整可用动作 ---
- lian_xu_wen_ben_hui_fu = 0 # 连续文本回复次数
actions_to_remove_temporarily = []
- probability_roll = random.random() # 在循环外掷骰子一次,用于概率判断
-
- # 反向遍历最近的循环历史
+ # --- 检查历史动作并决定临时移除动作 (逻辑保持不变) ---
+ lian_xu_wen_ben_hui_fu = 0
+ probability_roll = random.random()
for cycle in reversed(self._cycle_history):
- # 只关心实际执行了动作的循环
if cycle.action_taken:
if cycle.action_type == "text_reply":
lian_xu_wen_ben_hui_fu += 1
else:
- break # 遇到非文本回复,中断计数
- # 检查最近的3个循环即可,避免检查过多历史 (如果历史很长)
+ break
if len(self._cycle_history) > 0 and cycle.cycle_id <= self._cycle_history[0].cycle_id + (
len(self._cycle_history) - 4
):
break
-
logger.debug(f"{self.log_prefix}[Planner] 检测到连续文本回复次数: {lian_xu_wen_ben_hui_fu}")
- # 根据连续次数决定临时移除哪些动作
if lian_xu_wen_ben_hui_fu >= 3:
logger.info(f"{self.log_prefix}[Planner] 连续回复 >= 3 次,强制移除 text_reply 和 emoji_reply")
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
elif lian_xu_wen_ben_hui_fu == 2:
- if probability_roll < 0.8: # 80% 概率
+ if probability_roll < 0.8:
logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (触发)")
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
else:
@@ -826,168 +800,179 @@ class HeartFChatting:
f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (未触发)"
)
elif lian_xu_wen_ben_hui_fu == 1:
- if probability_roll < 0.4: # 40% 概率
+ if probability_roll < 0.4:
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次,40% 概率移除 text_reply (触发)")
actions_to_remove_temporarily.append("text_reply")
else:
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次,40% 概率移除 text_reply (未触发)")
- # 如果 lian_xu_wen_ben_hui_fu == 0,则不移除任何动作
- # --- 结束:检查历史动作 ---
+ # --- 结束检查历史动作 ---
# 获取观察信息
observation = self.observations[0]
- if is_re_planned:
- await observation.observe()
+ # if is_re_planned: # 暂时简化,不处理重新规划
+ # await observation.observe()
observed_messages = observation.talking_message
observed_messages_str = observation.talking_message_str_truncate
- # --- 使用 LLM 进行决策 --- #
- reasoning = "默认决策或获取决策失败"
- llm_error = False # LLM错误标志
- arguments = None # 初始化参数变量
- emoji_query = "" # <--- 在这里初始化 emoji_query
+ # --- 使用 LLM 进行决策 (JSON 输出模式) --- #
+ action = "no_reply" # 默认动作
+ reasoning = "规划器初始化默认"
+ emoji_query = ""
+ llm_error = False # LLM 请求或解析错误标志
+
+ # 获取我们将传递给 prompt 构建器和用于验证的当前可用动作
+ current_available_actions = self.action_manager.get_available_actions()
try:
- # --- 新增:应用临时动作移除 ---
+ # --- 应用临时动作移除 ---
if actions_to_remove_temporarily:
self.action_manager.temporarily_remove_actions(actions_to_remove_temporarily)
+ # 更新 current_available_actions 以反映移除后的状态
+ current_available_actions = self.action_manager.get_available_actions()
logger.debug(
- f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(self.action_manager.get_available_actions().keys())}"
+ f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(current_available_actions.keys())}"
)
- # --- 构建提示词 ---
- replan_prompt_str = ""
- if is_re_planned:
- replan_prompt_str = await self._build_replan_prompt(
- self._current_cycle.action_type, self._current_cycle.reasoning
- )
+ # --- 构建提示词 (调用修改后的 _build_planner_prompt) ---
+ # replan_prompt_str = "" # 暂时简化
+ # if is_re_planned:
+ # replan_prompt_str = await self._build_replan_prompt(
+ # self._current_cycle.action_type, self._current_cycle.reasoning
+ # )
prompt = await self._build_planner_prompt(
- observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt_str
+ observed_messages_str,
+ current_mind,
+ self.sub_mind.structured_info,
+ "", # replan_prompt_str,
+ current_available_actions, # <--- 传入当前可用动作
)
- # --- 调用 LLM ---
+ # --- 调用 LLM (普通文本生成) ---
+ llm_content = None
try:
- planner_tools = self.action_manager.get_planner_tool_definition()
- logger.debug(f"{self.log_prefix}[Planner] 本次使用的工具定义: {planner_tools}") # 记录本次使用的工具
- _response_text, _reasoning_content, tool_calls = await self.planner_llm.generate_response_tool_async(
- prompt=prompt,
- tools=planner_tools,
- )
- logger.debug(f"{self.log_prefix}[Planner] 原始人 LLM响应: {_response_text}")
+ # 假设 LLMRequest 有 generate_response 方法返回 (content, reasoning, model_name)
+ # 我们只需要 content
+ # !! 注意:这里假设 self.planner_llm 有 generate_response 方法
+ # !! 如果你的 LLMRequest 类使用的是其他方法名,请相应修改
+ llm_content, _, _ = await self.planner_llm.generate_response(prompt=prompt)
+ logger.debug(f"{self.log_prefix}[Planner] LLM 原始 JSON 响应 (预期): {llm_content}")
except Exception as req_e:
- logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}")
- action = "error"
- reasoning = f"LLM请求失败: {req_e}"
+ logger.error(f"{self.log_prefix}[Planner] LLM 请求执行失败: {req_e}")
+ reasoning = f"LLM 请求失败: {req_e}"
llm_error = True
- # 直接返回错误结果
- return {
- "action": action,
- "reasoning": reasoning,
- "emoji_query": "",
- "current_mind": current_mind,
- "observed_messages": observed_messages,
- "llm_error": llm_error,
- }
+ # 直接使用默认动作返回错误结果
+ action = "no_reply" # 明确设置为默认值
+ emoji_query = "" # 明确设置为空
+ # 不再立即返回,而是继续执行 finally 块以恢复动作
+ # return { ... }
- # 默认错误状态
- action = "error"
- reasoning = "处理工具调用时出错"
- llm_error = True
+ # --- 解析 LLM 返回的 JSON (仅当 LLM 请求未出错时进行) ---
+ if not llm_error and llm_content:
+ try:
+ # 尝试去除可能的 markdown 代码块标记
+ cleaned_content = (
+ llm_content.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
+ )
+ if not cleaned_content:
+ raise json.JSONDecodeError("Cleaned content is empty", cleaned_content, 0)
+ parsed_json = json.loads(cleaned_content)
- # 1. 验证工具调用
- success, valid_tool_calls, error_msg = process_llm_tool_calls(
- tool_calls, log_prefix=f"{self.log_prefix}[Planner] "
- )
+ # 提取决策,提供默认值
+ extracted_action = parsed_json.get("action", "no_reply")
+ extracted_reasoning = parsed_json.get("reasoning", "LLM未提供理由")
+ extracted_emoji_query = parsed_json.get("emoji_query", "")
- if success and valid_tool_calls:
- # 2. 提取第一个调用并获取参数
- first_tool_call = valid_tool_calls[0]
- tool_name = first_tool_call.get("function", {}).get("name")
- arguments = extract_tool_call_arguments(first_tool_call, None)
-
- # 3. 检查名称和参数
- expected_tool_name = "decide_reply_action"
- if tool_name == expected_tool_name and arguments is not None:
- # 4. 成功,提取决策
- extracted_action = arguments.get("action", "no_reply")
- # 验证动作
- if extracted_action not in self.action_manager.get_available_actions():
- # 如果LLM返回了一个此时不该用的动作(因为被临时移除了)
- # 或者完全无效的动作
+ # 验证动作是否在当前可用列表中
+ # !! 使用调用 prompt 时实际可用的动作列表进行验证
+ if extracted_action not in current_available_actions:
logger.warning(
- f"{self.log_prefix}[Planner] LLM返回了当前不可用或无效的动作: {extracted_action},将强制使用 'no_reply'"
+ f"{self.log_prefix}[Planner] LLM 返回了当前不可用或无效的动作: '{extracted_action}' (可用: {list(current_available_actions.keys())}),将强制使用 'no_reply'"
)
action = "no_reply"
- reasoning = f"LLM返回了当前不可用的动作: {extracted_action}"
+ reasoning = f"LLM 返回了当前不可用的动作 '{extracted_action}' (可用: {list(current_available_actions.keys())})。原始理由: {extracted_reasoning}"
emoji_query = ""
- llm_error = False # 视为逻辑修正而非 LLM 错误
- # --- 检查 'no_reply' 是否也恰好被移除了 (极端情况) ---
- if "no_reply" not in self.action_manager.get_available_actions():
+ # 检查 no_reply 是否也恰好被移除了 (极端情况)
+ if "no_reply" not in current_available_actions:
logger.error(
f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。"
)
action = "error" # 回退到错误状态
reasoning = "无法执行任何有效动作,包括 no_reply"
- llm_error = True
+ llm_error = True # 标记为严重错误
+ else:
+ llm_error = False # 视为逻辑修正而非 LLM 错误
else:
- # 动作有效且可用,使用提取的值
+ # 动作有效且可用
action = extracted_action
- reasoning = arguments.get("reasoning", "未提供理由")
- emoji_query = arguments.get("emoji_query", "")
- llm_error = False # 成功处理
- # 记录决策结果
+ reasoning = extracted_reasoning
+ emoji_query = extracted_emoji_query
+ llm_error = False # 解析成功
logger.debug(
- f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
+ f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果 (来自JSON): {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
)
- elif tool_name != expected_tool_name:
- reasoning = f"LLM返回了非预期的工具: {tool_name}"
- logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
- else: # arguments is None
- reasoning = f"无法提取工具 {tool_name} 的参数"
- logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
- elif not success:
- reasoning = f"验证工具调用失败: {error_msg}"
- logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
- else: # not valid_tool_calls
- # 如果没有有效的工具调用,我们需要检查 'no_reply' 是否是当前唯一可用的动作
- available_actions = list(self.action_manager.get_available_actions().keys())
- if available_actions == ["no_reply"]:
- logger.info(
- f"{self.log_prefix}[Planner] LLM未返回工具调用,但当前唯一可用动作是 'no_reply',将执行 'no_reply'"
- )
- action = "no_reply"
- reasoning = "LLM未返回工具调用,且当前仅 'no_reply' 可用"
- emoji_query = ""
- llm_error = False # 视为逻辑选择而非错误
- else:
- reasoning = "LLM未返回有效的工具调用"
- logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
- # llm_error 保持为 True
- # 如果 llm_error 仍然是 True,说明在处理过程中有错误发生
- except Exception as llm_e:
- logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中发生意外错误: {llm_e}")
+ except json.JSONDecodeError as json_e:
+ logger.warning(
+ f"{self.log_prefix}[Planner] 解析LLM响应JSON失败: {json_e}. LLM原始输出: '{llm_content}'"
+ )
+ reasoning = f"解析LLM响应JSON失败: {json_e}. 将使用默认动作 'no_reply'."
+ action = "no_reply" # 解析失败则默认不回复
+ emoji_query = ""
+ llm_error = True # 标记解析错误
+ except Exception as parse_e:
+ logger.error(f"{self.log_prefix}[Planner] 处理LLM响应时发生意外错误: {parse_e}")
+ reasoning = f"处理LLM响应时发生意外错误: {parse_e}. 将使用默认动作 'no_reply'."
+ action = "no_reply"
+ emoji_query = ""
+ llm_error = True
+ elif not llm_error and not llm_content:
+ # LLM 请求成功但返回空内容
+ logger.warning(f"{self.log_prefix}[Planner] LLM 返回了空内容。")
+ reasoning = "LLM 返回了空内容,使用默认动作 'no_reply'."
+ action = "no_reply"
+ emoji_query = ""
+ llm_error = True # 标记为空响应错误
+
+ # 如果 llm_error 在此阶段为 True,意味着请求成功但解析失败或返回空
+ # 如果 llm_error 在请求阶段就为 True,则跳过了此解析块
+
+ except Exception as outer_e:
+ logger.error(f"{self.log_prefix}[Planner] Planner 处理过程中发生意外错误: {outer_e}")
logger.error(traceback.format_exc())
- action = "error"
- reasoning = f"Planner内部处理错误: {llm_e}"
+ action = "error" # 发生未知错误,标记为 error 动作
+ reasoning = f"Planner 内部处理错误: {outer_e}"
+ emoji_query = ""
llm_error = True
- # --- 新增:确保动作恢复 ---
finally:
- if actions_to_remove_temporarily: # 只有当确实移除了动作时才需要恢复
+ # --- 确保动作恢复 ---
+ # 检查 self._original_actions_backup 是否有值来判断是否需要恢复
+ if self.action_manager._original_actions_backup is not None:
self.action_manager.restore_actions()
logger.debug(
f"{self.log_prefix}[Planner] 恢复了原始动作集, 当前可用: {list(self.action_manager.get_available_actions().keys())}"
)
- # --- 结束:确保动作恢复 ---
- # --- 结束 LLM 决策 --- #
+ # --- 结束确保动作恢复 ---
+ # --- 概率性忽略文本回复附带的表情 (逻辑保持不变) ---
+ if action == "text_reply" and emoji_query:
+ logger.debug(f"{self.log_prefix}[Planner] 大模型建议文字回复带表情: '{emoji_query}'")
+ if random.random() > EMOJI_SEND_PRO:
+ logger.info(
+ f"{self.log_prefix}但是麦麦这次不想加表情 ({1 - EMOJI_SEND_PRO:.0%}),忽略表情 '{emoji_query}'"
+ )
+ emoji_query = "" # 清空表情请求
+ else:
+ logger.info(f"{self.log_prefix}好吧,加上表情 '{emoji_query}'")
+ # --- 结束概率性忽略 ---
+
+ # 返回结果字典
return {
"action": action,
"reasoning": reasoning,
"emoji_query": emoji_query,
"current_mind": current_mind,
"observed_messages": observed_messages,
- "llm_error": llm_error,
+ "llm_error": llm_error, # 返回错误状态
}
async def _get_anchor_message(self) -> Optional[MessageRecv]:
@@ -1016,9 +1001,7 @@ class HeartFChatting:
}
anchor_message = MessageRecv(placeholder_msg_dict)
anchor_message.update_chat_stream(self.chat_stream)
- logger.info(
- f"{self.log_prefix} Created placeholder anchor message: ID={anchor_message.message_info.message_id}"
- )
+ logger.debug(f"{self.log_prefix} 创建占位符锚点消息: ID={anchor_message.message_info.message_id}")
return anchor_message
except Exception as e:
@@ -1131,8 +1114,9 @@ class HeartFChatting:
current_mind: Optional[str],
structured_info: Dict[str, Any],
replan_prompt: str,
+ current_available_actions: Dict[str, str],
) -> str:
- """构建 Planner LLM 的提示词"""
+ """构建 Planner LLM 的提示词 (获取模板并填充数据)"""
try:
# 准备结构化信息块
structured_info_block = ""
@@ -1148,12 +1132,13 @@ class HeartFChatting:
else:
chat_content_block = "当前没有观察到新的聊天内容。\n"
- # 准备当前思维块
+ # 准备当前思维块 (修改以匹配模板)
current_mind_block = ""
if current_mind:
- current_mind_block = f"{current_mind}"
+ # 模板中占位符是 {current_mind_block},它期望包含"你的内心想法:"的前缀
+ current_mind_block = f"你的内心想法:\n{current_mind}"
else:
- current_mind_block = "[没有特别的想法]"
+ current_mind_block = "你的内心想法:\n[没有特别的想法]"
# 准备循环信息块 (分析最近的活动循环)
recent_active_cycles = []
@@ -1193,23 +1178,40 @@ class HeartFChatting:
# 包装提示块,增加可读性,即使没有连续回复也给个标记
if cycle_info_block:
+ # 模板中占位符是 {cycle_info_block},它期望包含"【近期回复历史】"的前缀
cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n"
else:
# 如果最近的活动循环不是文本回复,或者没有活动循环
cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n"
individuality = Individuality.get_instance()
+ # 模板中占位符是 {prompt_personality}
prompt_personality = individuality.get_prompt(x_person=2, level=2)
- # 获取提示词模板并填充数据
- prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format(
+ # --- 构建可用动作描述 (用于填充模板中的 {action_options_text}) ---
+ action_options_text = "当前你可以选择的行动有:\n"
+ action_keys = list(current_available_actions.keys())
+ for name in action_keys:
+ desc = current_available_actions[name]
+ action_options_text += f"- '{name}': {desc}\n"
+
+ # --- 选择一个示例动作键 (用于填充模板中的 {example_action}) ---
+ example_action_key = action_keys[0] if action_keys else "no_reply"
+
+ # --- 获取提示词模板 ---
+ planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt")
+
+ # --- 填充模板 ---
+ prompt = planner_prompt_template.format(
bot_name=global_config.BOT_NICKNAME,
prompt_personality=prompt_personality,
structured_info_block=structured_info_block,
chat_content_block=chat_content_block,
current_mind_block=current_mind_block,
- replan=replan_prompt,
+ replan="", # 暂时留空 replan 信息
cycle_info_block=cycle_info_block,
+ action_options_text=action_options_text, # 传入可用动作描述
+ example_action=example_action_key, # 传入示例动作键
)
return prompt
@@ -1217,7 +1219,7 @@ class HeartFChatting:
except Exception as e:
logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}")
logger.error(traceback.format_exc())
- return ""
+ return "[构建 Planner Prompt 时出错]" # 返回错误提示,避免空字符串
# --- 回复器 (Replier) 的定义 --- #
async def _replier_work(
@@ -1258,7 +1260,7 @@ class HeartFChatting:
try:
with Timer("LLM生成", {}): # 内部计时器,可选保留
content, reasoning_content, model_name = await self.model_normal.generate_response(prompt)
- logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n")
+ # logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n")
# 捕捉 LLM 输出信息
info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name
diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py
index 429e96975..40819f013 100644
--- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py
+++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py
@@ -47,17 +47,15 @@ def init_prompt():
"info_from_tools",
)
- # Planner提示词 - 优化版
+ # Planner提示词 - 修改为要求 JSON 输出
Prompt(
"""你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话:
{structured_info_block}
{chat_content_block}
-你的内心想法:
{current_mind_block}
-{replan}
{cycle_info_block}
-请综合分析聊天内容和你看到的新消息,参考内心想法,使用'decide_reply_action'工具做出决策。决策时请注意:
+请综合分析聊天内容和你看到的新消息,参考内心想法,并根据以下原则和可用动作做出决策。
【回复原则】
1. 不回复(no_reply)适用:
@@ -69,7 +67,7 @@ def init_prompt():
2. 文字回复(text_reply)适用:
- 有实质性内容需要表达
- 有人提到你,但你还没有回应他
-- 可以追加emoji_query表达情绪(格式:情绪描述,如"俏皮的调侃")
+- 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合,也就是当前场合)
- 不要追加太多表情
3. 纯表情回复(emoji_reply)适用:
@@ -81,14 +79,34 @@ def init_prompt():
- 避免重复或评价自己的发言
- 不要和自己聊天
-【必须遵守】
-- 遵守回复原则
-- 必须调用工具并包含action和reasoning
-- 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply)
-- 并不是所有选择都可用
-- 选择text_reply或emoji_reply时必须提供emoji_query
-- 保持回复自然,符合日常聊天习惯""",
- "planner_prompt",
+【决策任务】
+{action_options_text}
+
+你必须从上面列出的可用行动中选择一个,并说明原因。
+你的决策必须以严格的 JSON 格式输出,且仅包含 JSON 内容,不要有任何其他文字或解释。
+JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
+{{
+ "action": "string", // 必须是上面提供的可用行动之一 (例如: '{example_action}')
+ "reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则
+ "emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。
+}}
+
+例如:
+{{
+ "action": "text_reply",
+ "reasoning": "用户提到了我,且问题比较具体,适合用文本回复。考虑到内容,可以带上一个微笑表情。",
+ "emoji_query": "微笑"
+}}
+或
+{{
+ "action": "no_reply",
+ "reasoning": "我已经连续回复了两次,而且这个话题我不太感兴趣,根据回复原则,选择不回复,等待其他人发言。",
+ "emoji_query": ""
+}}
+
+请输出你的决策 JSON:
+""", # 使用三引号避免内部引号问题
+ "planner_prompt", # 保持名称不变,替换内容
)
Prompt(
@@ -177,7 +195,7 @@ class PromptBuilder:
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id,
timestamp=time.time(),
- limit=global_config.MAX_CONTEXT_SIZE,
+ limit=global_config.observation_context_size,
)
chat_talking_prompt = await build_readable_messages(
@@ -246,6 +264,8 @@ class PromptBuilder:
sender_name=sender_name,
)
+ logger.debug(f"focus_chat_prompt: \n{prompt}")
+
return prompt
async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]:
@@ -259,15 +279,15 @@ class PromptBuilder:
who_chat_in_group += get_recent_group_speaker(
chat_stream.stream_id,
(chat_stream.user_info.platform, chat_stream.user_info.user_id),
- limit=global_config.MAX_CONTEXT_SIZE,
+ limit=global_config.observation_context_size,
)
relation_prompt = ""
for person in who_chat_in_group:
relation_prompt += await relationship_manager.build_relationship_info(person)
- print(f"relation_prompt: {relation_prompt}")
+ # print(f"relation_prompt: {relation_prompt}")
- print(f"relat11111111ion_prompt: {relation_prompt}")
+ # print(f"relat11111111ion_prompt: {relation_prompt}")
# 心情
mood_manager = MoodManager.get_instance()
@@ -318,7 +338,7 @@ class PromptBuilder:
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id,
timestamp=time.time(),
- limit=global_config.MAX_CONTEXT_SIZE,
+ limit=global_config.observation_context_size,
)
chat_talking_prompt = await build_readable_messages(
diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py
index e97cbcd2e..9ed63c2df 100644
--- a/src/plugins/heartFC_chat/normal_chat.py
+++ b/src/plugins/heartFC_chat/normal_chat.py
@@ -44,6 +44,8 @@ class NormalChat:
# 存储此实例的兴趣监控任务
self.start_time = time.time()
+ self.last_speak_time = 0
+
self._chat_task: Optional[asyncio.Task] = None
logger.info(f"[{self.stream_name}] NormalChat 实例初始化完成。")
@@ -119,6 +121,8 @@ class NormalChat:
await message_manager.add_message(message_set)
+ self.last_speak_time = time.time()
+
return first_bot_msg
# 改为实例方法
@@ -354,7 +358,9 @@ class NormalChat:
processed_count = 0
# --- 修改:迭代前创建要处理的ID列表副本,防止迭代时修改 ---
messages_to_process_initially = list(messages_to_reply) # 创建副本
- # --- 修改结束 ---
+ # --- 新增:限制最多处理两条消息 ---
+ messages_to_process_initially = messages_to_process_initially[:2]
+ # --- 新增结束 ---
for item in messages_to_process_initially: # 使用副本迭代
msg_id, (message, interest_value, is_mentioned) = item
# --- 修改:在处理前尝试 pop,防止竞争 ---
@@ -439,7 +445,7 @@ class NormalChat:
logger.error(f"[{self.stream_name}] 任务异常: {exc}")
logger.error(traceback.format_exc())
except asyncio.CancelledError:
- logger.info(f"[{self.stream_name}] 任务已取消")
+ logger.debug(f"[{self.stream_name}] 任务已取消")
except Exception as e:
logger.error(f"[{self.stream_name}] 回调处理错误: {e}")
finally:
@@ -452,12 +458,12 @@ class NormalChat:
"""停止当前实例的兴趣监控任务。"""
if self._chat_task and not self._chat_task.done():
task = self._chat_task
- logger.info(f"[{self.stream_name}] 尝试取消聊天任务。")
+ logger.debug(f"[{self.stream_name}] 尝试取消normal聊天任务。")
task.cancel()
try:
await task # 等待任务响应取消
except asyncio.CancelledError:
- logger.info(f"[{self.stream_name}] 聊天任务已成功取消。")
+ logger.info(f"[{self.stream_name}] 结束一般聊天模式。")
except Exception as e:
# 回调函数 _handle_task_completion 会处理异常日志
logger.warning(f"[{self.stream_name}] 等待监控任务取消时捕获到异常 (可能已在回调中记录): {e}")
diff --git a/src/plugins/heartFC_chat/normal_chat_generator.py b/src/plugins/heartFC_chat/normal_chat_generator.py
index 0ad9707f9..ea698bf2c 100644
--- a/src/plugins/heartFC_chat/normal_chat_generator.py
+++ b/src/plugins/heartFC_chat/normal_chat_generator.py
@@ -29,7 +29,7 @@ class NormalChatGenerator:
)
self.model_sum = LLMRequest(
- model=global_config.llm_summary_by_topic, temperature=0.7, max_tokens=3000, request_type="relation"
+ model=global_config.llm_summary, temperature=0.7, max_tokens=3000, request_type="relation"
)
self.current_model_type = "r1" # 默认使用 R1
self.current_model_name = "unknown model"
@@ -82,12 +82,14 @@ class NormalChatGenerator:
sender_name=sender_name,
chat_stream=message.chat_stream,
)
- logger.info(f"构建prompt时间: {t_build_prompt.human_readable}")
+ logger.debug(f"构建prompt时间: {t_build_prompt.human_readable}")
try:
content, reasoning_content, self.current_model_name = await model.generate_response(prompt)
- logger.info(f"prompt:{prompt}\n生成回复:{content}")
+ logger.debug(f"prompt:{prompt}\n生成回复:{content}")
+
+ logger.info(f"对 {message.processed_plain_text} 的回复:{content}")
info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name
diff --git a/src/plugins/knowledge/src/qa_manager.py b/src/plugins/knowledge/src/qa_manager.py
index 9cb5c0186..a09879a17 100644
--- a/src/plugins/knowledge/src/qa_manager.py
+++ b/src/plugins/knowledge/src/qa_manager.py
@@ -11,6 +11,9 @@ from .lpmmconfig import global_config
from .utils.dyn_topk import dyn_select_top_k
+MAX_KNOWLEDGE_LENGTH = 10000 # 最大知识长度
+
+
class QAManager:
def __init__(
self,
@@ -112,8 +115,10 @@ class QAManager:
for res in query_res
]
found_knowledge = "\n".join(
- [f"第{i + 1}条知识:{k[1]}\n 该条知识对于问题的相关性:{k[0]}" for i, k in enumerate(knowledge)]
+ [f"第{i + 1}条知识:{k[0]}\n 该条知识对于问题的相关性:{k[1]}" for i, k in enumerate(knowledge)]
)
+ if len(found_knowledge) > MAX_KNOWLEDGE_LENGTH:
+ found_knowledge = found_knowledge[:MAX_KNOWLEDGE_LENGTH] + "\n"
return found_knowledge
else:
logger.info("LPMM知识库并未初始化,使用旧版数据库进行检索")
diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py
index e5aa096fa..7a5fc1a82 100644
--- a/src/plugins/memory_system/Hippocampus.py
+++ b/src/plugins/memory_system/Hippocampus.py
@@ -189,7 +189,7 @@ class Hippocampus:
def __init__(self):
self.memory_graph = MemoryGraph()
self.llm_topic_judge = None
- self.llm_summary_by_topic = None
+ self.llm_summary = None
self.entorhinal_cortex = None
self.parahippocampal_gyrus = None
self.config = None
@@ -203,7 +203,7 @@ class Hippocampus:
# 从数据库加载记忆图
self.entorhinal_cortex.sync_memory_from_db()
self.llm_topic_judge = LLMRequest(self.config.llm_topic_judge, request_type="memory")
- self.llm_summary_by_topic = LLMRequest(self.config.llm_summary_by_topic, request_type="memory")
+ self.llm_summary = LLMRequest(self.config.llm_summary, request_type="memory")
def get_all_node_names(self) -> list:
"""获取记忆图中所有节点的名字列表"""
@@ -1169,7 +1169,7 @@ class ParahippocampalGyrus:
# 调用修改后的 topic_what,不再需要 time_info
topic_what_prompt = self.hippocampus.topic_what(input_text, topic)
try:
- task = self.hippocampus.llm_summary_by_topic.generate_response_async(topic_what_prompt)
+ task = self.hippocampus.llm_summary.generate_response_async(topic_what_prompt)
tasks.append((topic.strip(), task))
except Exception as e:
logger.error(f"生成话题 '{topic}' 的摘要时发生错误: {e}")
diff --git a/src/plugins/memory_system/memory_config.py b/src/plugins/memory_system/memory_config.py
index b2fb72801..b82e54ec1 100644
--- a/src/plugins/memory_system/memory_config.py
+++ b/src/plugins/memory_system/memory_config.py
@@ -24,7 +24,7 @@ class MemoryConfig:
consolidate_memory_interval: int # 记忆整合间隔
llm_topic_judge: str # 话题判断模型
- llm_summary_by_topic: str # 话题总结模型
+ llm_summary: str # 话题总结模型
@classmethod
def from_global_config(cls, global_config):
@@ -44,7 +44,5 @@ class MemoryConfig:
consolidate_memory_percentage=getattr(global_config, "consolidate_memory_percentage", 0.01),
consolidate_memory_interval=getattr(global_config, "consolidate_memory_interval", 1000),
llm_topic_judge=getattr(global_config, "llm_topic_judge", "default_judge_model"), # 添加默认模型名
- llm_summary_by_topic=getattr(
- global_config, "llm_summary_by_topic", "default_summary_model"
- ), # 添加默认模型名
+ llm_summary=getattr(global_config, "llm_summary", "default_summary_model"), # 添加默认模型名
)
diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py
index e8ccf1ca5..e421641c3 100644
--- a/src/plugins/models/utils_model.py
+++ b/src/plugins/models/utils_model.py
@@ -632,7 +632,7 @@ class LLMRequest:
**params_copy,
}
if "max_tokens" not in payload and "max_completion_tokens" not in payload:
- payload["max_tokens"] = global_config.max_response_length
+ payload["max_tokens"] = global_config.model_max_output_length
# 如果 payload 中依然存在 max_tokens 且需要转换,在这里进行再次检查
if self.model_name.lower() in self.MODELS_NEEDING_TRANSFORMATION and "max_tokens" in payload:
payload["max_completion_tokens"] = payload.pop("max_tokens")
diff --git a/src/plugins/person_info/relationship_manager.py b/src/plugins/person_info/relationship_manager.py
index a25dd731c..fc8cf548e 100644
--- a/src/plugins/person_info/relationship_manager.py
+++ b/src/plugins/person_info/relationship_manager.py
@@ -282,10 +282,10 @@ class RelationshipManager:
if is_id:
person_id = person
else:
- print(f"person: {person}")
+ # print(f"person: {person}")
person_id = person_info_manager.get_person_id(person[0], person[1])
person_name = await person_info_manager.get_value(person_id, "person_name")
- print(f"person_name: {person_name}")
+ # print(f"person_name: {person_name}")
relationship_value = await person_info_manager.get_value(person_id, "relationship_value")
level_num = self.calculate_level_num(relationship_value)
diff --git a/src/plugins/respon_info_catcher/info_catcher.py b/src/plugins/respon_info_catcher/info_catcher.py
index 4b19844a8..5cb67a16d 100644
--- a/src/plugins/respon_info_catcher/info_catcher.py
+++ b/src/plugins/respon_info_catcher/info_catcher.py
@@ -8,13 +8,12 @@ from typing import List
class InfoCatcher:
def __init__(self):
- self.chat_history = [] # 聊天历史,长度为三倍使用的上下文
- self.context_length = global_config.MAX_CONTEXT_SIZE
- self.chat_history_in_thinking = [] # 思考期间的聊天内容
- self.chat_history_after_response = [] # 回复后的聊天内容,长度为一倍上下文
+ self.chat_history = [] # 聊天历史,长度为三倍使用的上下文喵~
+ self.context_length = global_config.observation_context_size
+ self.chat_history_in_thinking = [] # 思考期间的聊天内容喵~
+ self.chat_history_after_response = [] # 回复后的聊天内容,长度为一倍上下文喵~
self.chat_id = ""
- self.response_mode = global_config.response_mode
self.trigger_response_text = ""
self.response_text = ""
@@ -36,10 +35,10 @@ class InfoCatcher:
"model": "",
}
- # 使用字典来存储 reasoning 模式的数据
+ # 使用字典来存储 reasoning 模式的数据喵~
self.reasoning_data = {"thinking_log": "", "prompt": "", "response": "", "model": ""}
- # 耗时
+ # 耗时喵~
self.timing_results = {
"interested_rate_time": 0,
"sub_heartflow_observe_time": 0,
@@ -73,15 +72,25 @@ class InfoCatcher:
self.heartflow_data["sub_heartflow_now"] = current_mind
def catch_after_llm_generated(self, prompt: str, response: str, reasoning_content: str = "", model_name: str = ""):
- if self.response_mode == "heart_flow":
- self.heartflow_data["prompt"] = prompt
- self.heartflow_data["response"] = response
- self.heartflow_data["model"] = model_name
- elif self.response_mode == "reasoning":
- self.reasoning_data["thinking_log"] = reasoning_content
- self.reasoning_data["prompt"] = prompt
- self.reasoning_data["response"] = response
- self.reasoning_data["model"] = model_name
+ # if self.response_mode == "heart_flow": # 条件判断不需要了喵~
+ # self.heartflow_data["prompt"] = prompt
+ # self.heartflow_data["response"] = response
+ # self.heartflow_data["model"] = model_name
+ # elif self.response_mode == "reasoning": # 条件判断不需要了喵~
+ # self.reasoning_data["thinking_log"] = reasoning_content
+ # self.reasoning_data["prompt"] = prompt
+ # self.reasoning_data["response"] = response
+ # self.reasoning_data["model"] = model_name
+
+ # 直接记录信息喵~
+ self.reasoning_data["thinking_log"] = reasoning_content
+ self.reasoning_data["prompt"] = prompt
+ self.reasoning_data["response"] = response
+ self.reasoning_data["model"] = model_name
+ # 如果 heartflow 数据也需要通用字段,可以取消下面的注释喵~
+ # self.heartflow_data["prompt"] = prompt
+ # self.heartflow_data["response"] = response
+ # self.heartflow_data["model"] = model_name
self.response_text = response
@@ -172,13 +181,13 @@ class InfoCatcher:
}
def done_catch(self):
- """将收集到的信息存储到数据库的 thinking_log 集合中"""
+ """将收集到的信息存储到数据库的 thinking_log 集合中喵~"""
try:
- # 将消息对象转换为可序列化的字典
+ # 将消息对象转换为可序列化的字典喵~
thinking_log_data = {
"chat_id": self.chat_id,
- "response_mode": self.response_mode,
+ # "response_mode": self.response_mode, # 这个也删掉喵~
"trigger_text": self.trigger_response_text,
"response_text": self.response_text,
"trigger_info": {
@@ -195,18 +204,20 @@ class InfoCatcher:
"chat_history_after_response": self.message_list_to_dict(self.chat_history_after_response),
}
- # 根据不同的响应模式添加相应的数据
- if self.response_mode == "heart_flow":
- thinking_log_data["mode_specific_data"] = self.heartflow_data
- elif self.response_mode == "reasoning":
- thinking_log_data["mode_specific_data"] = self.reasoning_data
+ # 根据不同的响应模式添加相应的数据喵~ # 现在直接都加上去好了喵~
+ # if self.response_mode == "heart_flow":
+ # thinking_log_data["mode_specific_data"] = self.heartflow_data
+ # elif self.response_mode == "reasoning":
+ # thinking_log_data["mode_specific_data"] = self.reasoning_data
+ thinking_log_data["heartflow_data"] = self.heartflow_data
+ thinking_log_data["reasoning_data"] = self.reasoning_data
- # 将数据插入到 thinking_log 集合中
+ # 将数据插入到 thinking_log 集合中喵~
db.thinking_log.insert_one(thinking_log_data)
return True
except Exception as e:
- print(f"存储思考日志时出错: {str(e)}")
+ print(f"存储思考日志时出错: {str(e)} 喵~")
print(traceback.format_exc())
return False
diff --git a/src/plugins/utils/json_utils.py b/src/plugins/utils/json_utils.py
index ddcbca439..6226e6e96 100644
--- a/src/plugins/utils/json_utils.py
+++ b/src/plugins/utils/json_utils.py
@@ -1,6 +1,7 @@
import json
import logging
from typing import Any, Dict, TypeVar, List, Union, Tuple
+import ast
# 定义类型变量用于泛型类型提示
T = TypeVar("T")
@@ -12,6 +13,7 @@ logger = logging.getLogger("json_utils")
def safe_json_loads(json_str: str, default_value: T = None) -> Union[Any, T]:
"""
安全地解析JSON字符串,出错时返回默认值
+ 现在尝试处理单引号和标准JSON
参数:
json_str: 要解析的JSON字符串
@@ -20,16 +22,34 @@ def safe_json_loads(json_str: str, default_value: T = None) -> Union[Any, T]:
返回:
解析后的Python对象,或在解析失败时返回default_value
"""
- if not json_str:
+ if not json_str or not isinstance(json_str, str):
+ logger.warning(f"safe_json_loads 接收到非字符串输入: {type(json_str)}, 值: {json_str}")
return default_value
try:
+ # 尝试标准的 JSON 解析
return json.loads(json_str)
- except json.JSONDecodeError as e:
- logger.error(f"JSON解析失败: {e}, JSON字符串: {json_str[:100]}...")
- return default_value
+ except json.JSONDecodeError:
+ # 如果标准解析失败,尝试将单引号替换为双引号再解析
+ # (注意:这种替换可能不安全,如果字符串内容本身包含引号)
+ # 更安全的方式是用 ast.literal_eval
+ try:
+ # logger.debug(f"标准JSON解析失败,尝试用 ast.literal_eval 解析: {json_str[:100]}...")
+ result = ast.literal_eval(json_str)
+ # 确保结果是字典(因为我们通常期望参数是字典)
+ if isinstance(result, dict):
+ return result
+ else:
+ logger.warning(f"ast.literal_eval 解析成功但结果不是字典: {type(result)}, 内容: {result}")
+ return default_value
+ except (ValueError, SyntaxError, MemoryError, RecursionError) as ast_e:
+ logger.error(f"使用 ast.literal_eval 解析失败: {ast_e}, 字符串: {json_str[:100]}...")
+ return default_value
+ except Exception as e:
+ logger.error(f"使用 ast.literal_eval 解析时发生意外错误: {e}, 字符串: {json_str[:100]}...")
+ return default_value
except Exception as e:
- logger.error(f"JSON解析过程中发生意外错误: {e}")
+ logger.error(f"JSON解析过程中发生意外错误: {e}, 字符串: {json_str[:100]}...")
return default_value
@@ -177,25 +197,27 @@ def process_llm_tool_calls(
if "name" not in func_details or not isinstance(func_details.get("name"), str):
logger.warning(f"{log_prefix}工具调用[{i}]的'function'字段缺少'name'或类型不正确: {func_details}")
continue
- if "arguments" not in func_details or not isinstance(
- func_details.get("arguments"), str
- ): # 参数是字符串形式的JSON
- logger.warning(f"{log_prefix}工具调用[{i}]的'function'字段缺少'arguments'或类型不正确: {func_details}")
+
+ # 验证参数 'arguments'
+ args_value = func_details.get("arguments")
+
+ # 1. 检查 arguments 是否存在且是字符串
+ if args_value is None or not isinstance(args_value, str):
+ logger.warning(f"{log_prefix}工具调用[{i}]的'function'字段缺少'arguments'字符串: {func_details}")
continue
- # 可选:尝试解析参数JSON,确保其有效
- args_str = func_details["arguments"]
- try:
- json.loads(args_str) # 尝试解析,但不存储结果
- except json.JSONDecodeError as e:
+ # 2. 尝试安全地解析 arguments 字符串
+ parsed_args = safe_json_loads(args_value, None)
+
+ # 3. 检查解析结果是否为字典
+ if parsed_args is None or not isinstance(parsed_args, dict):
logger.warning(
- f"{log_prefix}工具调用[{i}]的'arguments'不是有效的JSON字符串: {e}, 内容: {args_str[:100]}..."
+ f"{log_prefix}工具调用[{i}]的'arguments'无法解析为有效的JSON字典, "
+ f"原始字符串: {args_value[:100]}..., 解析结果类型: {type(parsed_args).__name__}"
)
continue
- except Exception as e:
- logger.warning(f"{log_prefix}解析工具调用[{i}]的'arguments'时发生意外错误: {e}, 内容: {args_str[:100]}...")
- continue
+ # 如果检查通过,将原始的 tool_call 加入有效列表
valid_tool_calls.append(tool_call)
if not valid_tool_calls and tool_calls: # 如果原始列表不为空,但验证后为空
diff --git a/src/plugins/willing/mode_classical.py b/src/plugins/willing/mode_classical.py
index 8ee88d66f..e96aa77a7 100644
--- a/src/plugins/willing/mode_classical.py
+++ b/src/plugins/willing/mode_classical.py
@@ -64,6 +64,9 @@ class ClassicalWillingManager(BaseWillingManager):
self.chat_reply_willing[chat_id] = max(0, current_willing - 1.8)
async def after_generate_reply_handle(self, message_id):
+ if message_id not in self.ongoing_messages:
+ return
+
chat_id = self.ongoing_messages[message_id].chat_id
current_willing = self.chat_reply_willing.get(chat_id, 0)
if current_willing < 1:
diff --git a/src/plugins/willing/willing_manager.py b/src/plugins/willing/willing_manager.py
index 4b105ec95..c26325b1c 100644
--- a/src/plugins/willing/willing_manager.py
+++ b/src/plugins/willing/willing_manager.py
@@ -77,7 +77,7 @@ class BaseWillingManager(ABC):
if not issubclass(manager_class, cls):
raise TypeError(f"Manager class {manager_class.__name__} is not a subclass of {cls.__name__}")
else:
- logger.info(f"成功载入willing模式:{manager_type}")
+ logger.info(f"普通回复模式:{manager_type}")
return manager_class()
except (ImportError, AttributeError, TypeError) as e:
module = importlib.import_module(".mode_classical", __package__)
@@ -110,7 +110,7 @@ class BaseWillingManager(ABC):
def delete(self, message_id: str):
del_message = self.ongoing_messages.pop(message_id, None)
if not del_message:
- logger.debug(f"删除异常,当前消息{message_id}不存在")
+ logger.debug(f"尝试删除不存在的消息 ID: {message_id},可能已被其他流程处理,喵~")
@abstractmethod
async def async_task_starter(self) -> None:
diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml
index 8ca6aaa31..c924d35ab 100644
--- a/template/bot_config_template.toml
+++ b/template/bot_config_template.toml
@@ -1,5 +1,5 @@
[inner]
-version = "1.5.1"
+version = "1.6.0"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件,请在修改后将version的值进行变更
@@ -65,33 +65,14 @@ time_zone = "Asia/Shanghai" # 给你的机器人设置时区,可以解决运
[platforms] # 必填项目,填写每个平台适配器提供的链接
nonebot-qq="http://127.0.0.1:18002/api/message"
-[response] #群聊的回复策略
-#一般回复参数
-model_reasoning_probability = 0.7 # 麦麦回答时选择推理模型 模型的概率
-model_normal_probability = 0.3 # 麦麦回答时选择一般模型 模型的概率
-
-[heartflow]
-allow_focus_mode = true # 是否允许进入FOCUSED状态
-# 是否启用heart_flowC(心流聊天,HFC)模式
+[chat] #麦麦的聊天通用设置
+allow_focus_mode = true # 是否允许专注聊天状态
+# 是否启用heart_flowC(HFC)模式
# 启用后麦麦会自主选择进入heart_flowC模式(持续一段时间),进行主动的观察和回复,并给出回复,比较消耗token
-reply_trigger_threshold = 3.0 # 心流聊天触发阈值,越低越容易进入心流聊天
-probability_decay_factor_per_second = 0.2 # 概率衰减因子,越大衰减越快,越高越容易退出心流聊天
-default_decay_rate_per_second = 0.98 # 默认衰减率,越大衰减越快,越高越难进入心流聊天
+base_normal_chat_num = 3 # 最多允许多少个群进行普通聊天
+base_focused_chat_num = 2 # 最多允许多少个群进行专注聊天
-
-
-sub_heart_flow_stop_time = 500 # 子心流停止时间,超过这个时间没有回复,子心流会停止,间隔 单位秒
-
-observation_context_size = 20 # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩
-compressed_length = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度,超过心流观察到的上下文长度,会压缩,最短压缩长度为5
-compress_length_limit = 5 #最多压缩份数,超过该数值的压缩上下文会被删除
-
-
-[message]
-max_context_size = 12 # 麦麦回复时获得的上文数量,建议12,太短太长都会导致脑袋尖尖
-emoji_chance = 0.2 # 麦麦一般回复时使用表情包的概率,设置为1让麦麦自己决定发不发
-thinking_timeout = 100 # 麦麦最长思考时间,超过这个时间的思考会放弃(往往是api反应太慢)
-max_response_length = 256 # 麦麦单次回答的最大token数
+observation_context_size = 15 # 观察到的最长上下文大小,建议15,太短太长都会导致脑袋尖尖
message_buffer = true # 启用消息缓冲器?启用此项以解决消息的拆分问题,但会使麦麦的回复延迟
# 以下是消息过滤,可以根据规则过滤特定消息,将不会读取这些消息
@@ -106,7 +87,14 @@ ban_msgs_regex = [
# "\\[CQ:at,qq=\\d+\\]" # 匹配@
]
-[willing] # 一般回复模式的回复意愿设置
+[normal_chat] #普通聊天
+#一般回复参数
+model_reasoning_probability = 0.7 # 麦麦回答时选择推理模型 模型的概率
+model_normal_probability = 0.3 # 麦麦回答时选择一般模型 模型的概率
+
+emoji_chance = 0.2 # 麦麦一般回复时使用表情包的概率,设置为1让麦麦自己决定发不发
+thinking_timeout = 100 # 麦麦最长思考时间,超过这个时间的思考会放弃(往往是api反应太慢)
+
willing_mode = "classical" # 回复意愿模式 —— 经典模式:classical,动态模式:dynamic,mxp模式:mxp,自定义模式:custom(需要你自己实现)
response_willing_amplifier = 1 # 麦麦回复意愿放大系数,一般为1
response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数,听到记忆里的内容时放大系数
@@ -115,6 +103,16 @@ emoji_response_penalty = 0 # 表情包回复惩罚系数,设为0为不回复
mentioned_bot_inevitable_reply = false # 提及 bot 必然回复
at_bot_inevitable_reply = false # @bot 必然回复
+[focus_chat] #专注聊天
+reply_trigger_threshold = 3.6 # 专注聊天触发阈值,越低越容易进入专注聊天
+default_decay_rate_per_second = 0.95 # 默认衰减率,越大衰减越快,越高越难进入专注聊天
+consecutive_no_reply_threshold = 3 # 连续不回复的阈值,越低越容易结束专注聊天
+
+# 以下选项暂时无效
+compressed_length = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度,超过心流观察到的上下文长度,会压缩,最短压缩长度为5
+compress_length_limit = 5 #最多压缩份数,超过该数值的压缩上下文会被删除
+
+
[emoji]
max_emoji_num = 40 # 表情包最大数量
max_reach_deletion = true # 开启则在达到最大数量时删除表情包,关闭则达到最大数量时不删除,只是不会继续收集表情包
@@ -181,6 +179,8 @@ response_max_length = 256 # 回复允许的最大长度
response_max_sentence_num = 4 # 回复允许的最大句子数
enable_kaomoji_protection = false # 是否启用颜文字保护
+model_max_output_length = 256 # 模型单次返回的最大token数
+
[remote] #发送统计信息,主要是看全球有多少只麦麦
enable = true
@@ -197,55 +197,44 @@ pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与
# stream = : 用于指定模型是否是使用流式输出
# 如果不指定,则该项是 False
-[model.llm_reasoning] #只在回复模式为reasoning时启用
+#这个模型必须是推理模型
+[model.llm_reasoning] # 一般聊天模式的推理回复模型
name = "Pro/deepseek-ai/DeepSeek-R1"
-# name = "Qwen/QwQ-32B"
provider = "SILICONFLOW"
-pri_in = 4 #模型的输入价格(非必填,可以记录消耗)
-pri_out = 16 #模型的输出价格(非必填,可以记录消耗)
+pri_in = 1.0 #模型的输入价格(非必填,可以记录消耗)
+pri_out = 4.0 #模型的输出价格(非必填,可以记录消耗)
-#非推理模型
-
-[model.llm_normal] #V3 回复模型1 主要回复模型,默认temp 0.2 如果你使用的是老V3或者其他模型,请自己修改temp参数
+[model.llm_normal] #V3 回复模型 专注和一般聊天模式共用的回复模型
name = "Pro/deepseek-ai/DeepSeek-V3"
provider = "SILICONFLOW"
pri_in = 2 #模型的输入价格(非必填,可以记录消耗)
pri_out = 8 #模型的输出价格(非必填,可以记录消耗)
+#默认temp 0.2 如果你使用的是老V3或者其他模型,请自己修改temp参数
temp = 0.2 #模型的温度,新V3建议0.1-0.3
-[model.llm_emotion_judge] #表情包判断
-name = "Qwen/Qwen2.5-14B-Instruct"
-provider = "SILICONFLOW"
-pri_in = 0.7
-pri_out = 0.7
-
-[model.llm_topic_judge] #记忆主题判断:建议使用qwen2.5 7b
+[model.llm_topic_judge] #主题判断模型:建议使用qwen2.5 7b
name = "Pro/Qwen/Qwen2.5-7B-Instruct"
provider = "SILICONFLOW"
-pri_in = 0
-pri_out = 0
+pri_in = 0.35
+pri_out = 0.35
-[model.llm_summary_by_topic] #概括模型,建议使用qwen2.5 32b 及以上
+[model.llm_summary] #概括模型,建议使用qwen2.5 32b 及以上
name = "Qwen/Qwen2.5-32B-Instruct"
provider = "SILICONFLOW"
pri_in = 1.26
pri_out = 1.26
-[model.llm_tool_use] #工具调用模型,需要使用支持工具调用的模型,建议使用qwen2.5 32b
-name = "Qwen/Qwen2.5-32B-Instruct"
-provider = "SILICONFLOW"
-pri_in = 1.26
-pri_out = 1.26
-
-# 识图模型
-
-[model.vlm] #图像识别
+[model.vlm] # 图像识别模型
name = "Pro/Qwen/Qwen2.5-VL-7B-Instruct"
provider = "SILICONFLOW"
pri_in = 0.35
pri_out = 0.35
-
+[model.llm_heartflow] # 用于控制麦麦是否参与聊天的模型
+name = "Qwen/Qwen2.5-32B-Instruct"
+provider = "SILICONFLOW"
+pri_in = 1.26
+pri_out = 1.26
[model.llm_observation] #观察模型,压缩聊天内容,建议用免费的
# name = "Pro/Qwen/Qwen2.5-7B-Instruct"
@@ -254,19 +243,18 @@ provider = "SILICONFLOW"
pri_in = 0
pri_out = 0
-[model.llm_sub_heartflow] #子心流:认真水群时,生成麦麦的内心想法
-name = "Qwen/Qwen2.5-72B-Instruct"
+[model.llm_sub_heartflow] #心流:认真水群时,生成麦麦的内心想法,必须使用具有工具调用能力的模型
+name = "Pro/deepseek-ai/DeepSeek-V3"
provider = "SILICONFLOW"
-pri_in = 4.13
-pri_out = 4.13
-temp = 0.7 #模型的温度,新V3建议0.1-0.3
+pri_in = 2
+pri_out = 8
+temp = 0.3 #模型的温度,新V3建议0.1-0.3
-
-[model.llm_plan] #决策模型:认真水群时,负责决定麦麦该做什么
-name = "Qwen/Qwen2.5-32B-Instruct"
+[model.llm_plan] #决策:认真水群时,负责决定麦麦该做什么
+name = "Pro/deepseek-ai/DeepSeek-V3"
provider = "SILICONFLOW"
-pri_in = 1.26
-pri_out = 1.26
+pri_in = 2
+pri_out = 8
#嵌入模型
@@ -303,11 +291,13 @@ pri_in = 2
pri_out = 8
-#此模型暂时没有使用!!
-#此模型暂时没有使用!!
-#此模型暂时没有使用!!
-[model.llm_heartflow] #心流
-# name = "Pro/Qwen/Qwen2.5-7B-Instruct"
+#以下模型暂时没有使用!!
+#以下模型暂时没有使用!!
+#以下模型暂时没有使用!!
+#以下模型暂时没有使用!!
+#以下模型暂时没有使用!!
+
+[model.llm_tool_use] #工具调用模型,需要使用支持工具调用的模型,建议使用qwen2.5 32b
name = "Qwen/Qwen2.5-32B-Instruct"
provider = "SILICONFLOW"
pri_in = 1.26
diff --git a/(临时版)聊天兴趣监控.bat.bat b/(临时版)聊天兴趣监控.bat.bat
new file mode 100644
index 000000000..f26d14dee
--- /dev/null
+++ b/(临时版)聊天兴趣监控.bat.bat
@@ -0,0 +1,26 @@
+@echo off
+CHCP 65001 > nul
+setlocal enabledelayedexpansion
+
+REM 查找venv虚拟环境
+set "venv_path=%~dp0venv\Scripts\activate.bat"
+if not exist "%venv_path%" (
+ echo 错误: 未找到虚拟环境,请确保venv目录存在
+ pause
+ exit /b 1
+)
+
+REM 激活虚拟环境
+call "%venv_path%"
+if %ERRORLEVEL% neq 0 (
+ echo 错误: 虚拟环境激活失败
+ pause
+ exit /b 1
+)
+
+echo 虚拟环境已激活,正在启动 GUI...
+
+REM 运行 Python 脚本
+python scripts/interest_monitor_gui.py
+
+pause
\ No newline at end of file
diff --git a/(测试版)麦麦生成人格.bat b/(测试版)麦麦生成人格 copy.bat
similarity index 100%
rename from (测试版)麦麦生成人格.bat
rename to (测试版)麦麦生成人格 copy.bat