diff --git a/.gitignore b/.gitignore
index 67565521..c8518b2b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ llm_tool_benchmark_results.json
MaiBot-Napcat-Adapter-main
MaiBot-Napcat-Adapter
/test
+/log_debug
/src/test
nonebot-maibot-adapter/
*.zip
@@ -35,7 +36,6 @@ config/bot_config.toml
config/bot_config.toml.bak
config/lpmm_config.toml
config/lpmm_config.toml.bak
-src/plugins/remote/client_uuid.json
(测试版)麦麦生成人格.bat
(临时版)麦麦开始学习.bat
src/plugins/utils/statistic.py
@@ -43,7 +43,7 @@ src/plugins/utils/statistic.py
__pycache__/
*.py[cod]
*$py.class
-llm_statistics.txt
+maibot_statistics.html
mongodb
napcat
run_dev.bat
diff --git a/bot.py b/bot.py
index 8007329b..0e3439c1 100644
--- a/bot.py
+++ b/bot.py
@@ -1,7 +1,6 @@
import asyncio
import hashlib
import os
-import shutil
import sys
from pathlib import Path
import time
@@ -17,6 +16,8 @@ from rich.traceback import install
from src.plugins.group_nickname.nickname_manager import nickname_manager
import atexit
+from src.manager.async_task_manager import async_task_manager
+
install(extra_lines=3)
# 设置工作目录为脚本所在目录
@@ -66,38 +67,6 @@ def easter_egg():
print(rainbow_text)
-def init_config():
- # 初次启动检测
- if not os.path.exists("config/bot_config.toml"):
- logger.warning("检测到bot_config.toml不存在,正在从模板复制")
-
- # 检查config目录是否存在
- if not os.path.exists("config"):
- os.makedirs("config")
- logger.info("创建config目录")
-
- shutil.copy("template/bot_config_template.toml", "config/bot_config.toml")
- logger.info("复制完成,请修改config/bot_config.toml和.env中的配置后重新启动")
- if not os.path.exists("config/lpmm_config.toml"):
- logger.warning("检测到lpmm_config.toml不存在,正在从模板复制")
-
- # 检查config目录是否存在
- if not os.path.exists("config"):
- os.makedirs("config")
- logger.info("创建config目录")
-
- shutil.copy("template/lpmm_config_template.toml", "config/lpmm_config.toml")
- logger.info("复制完成,请修改config/lpmm_config.toml和.env中的配置后重新启动")
-
-
-def init_env():
- # 检测.env文件是否存在
- if not os.path.exists(".env"):
- logger.error("检测到.env文件不存在")
- shutil.copy("template/template.env", "./.env")
- logger.info("已从template/template.env复制创建.env,请修改配置后重新启动")
-
-
def load_env():
# 直接加载生产环境变量配置
if os.path.exists(".env"):
@@ -142,6 +111,10 @@ def scan_provider(env_config: dict):
async def graceful_shutdown():
try:
logger.info("正在优雅关闭麦麦...")
+
+ # 停止所有异步任务
+ await async_task_manager.stop_and_wait_all_tasks()
+
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
for task in tasks:
task.cancel()
@@ -237,9 +210,9 @@ def raw_main():
check_eula()
print("检查EULA和隐私条款完成")
+
easter_egg()
- init_config()
- init_env()
+
load_env()
env_config = {key: os.getenv(key) for key in os.environ}
diff --git a/src/config/config.py b/src/config/config.py
index 9a896cd2..5b021e1d 100644
--- a/src/config/config.py
+++ b/src/config/config.py
@@ -1,3 +1,6 @@
+# TODO: 更多的可配置项
+# TODO: 所有模型单独分离,温度可配置
+# TODO: 原生多模态支持
import os
import re
from dataclasses import dataclass, field
@@ -277,18 +280,44 @@ class BotConfig:
# experimental
enable_friend_chat: bool = False # 是否启用好友聊天
# enable_think_flow: bool = False # 是否启用思考流程
+ enable_friend_whitelist: bool = True # 是否启用好友白名单
talk_allowed_private = set()
- enable_pfc_chatting: bool = False # 是否启用PFC聊天
- enable_pfc_reply_checker: bool = True # 是否开启PFC回复检查
- pfc_message_buffer_size: int = (
- 2 # PFC 聊天消息缓冲数量,有利于使聊天节奏更加紧凑流畅,请根据实际 LLM 响应速度进行调整,默认2条
- )
api_polling_max_retries: int = 3 # 神秘小功能
rename_person: bool = (
True # 是否启用改名工具,可以让麦麦对唯一名进行更改,可能可以更拟人地称呼他人,但是也可能导致记忆混淆的问题
)
- # idle_chat
+ # pfc
+ enable_pfc_chatting: bool = False # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立
+ pfc_message_buffer_size: int = (
+ 2 # PFC 聊天消息缓冲数量,有利于使聊天节奏更加紧凑流畅,请根据实际 LLM 响应速度进行调整,默认2条
+ )
+ pfc_recent_history_display_count: int = 20 # PFC 对话最大可见上下文
+
+ # pfc.checker
+ enable_pfc_reply_checker: bool = True # 是否启用 PFC 的回复检查器
+ pfc_max_reply_attempts: int = 3 # 发言最多尝试次数
+ pfc_max_chat_history_for_checker: int = 50 # checker聊天记录最大可见上文长度
+
+ # pfc.emotion
+ pfc_emotion_update_intensity: float = 0.6 # 情绪更新强度
+ pfc_emotion_history_count: int = 5 # 情绪更新最大可见上下文长度
+
+ # pfc.relationship
+ pfc_relationship_incremental_interval: int = 10 # 关系值增值强度
+ pfc_relationship_incremental_msg_count: int = 10 # 会话中,关系值判断最大可见上下文
+ pfc_relationship_incremental_default_change: float = (
+ 1.0 # 会话中,关系值默认更新值(当 llm 返回错误时默认采用该值)
+ )
+ pfc_relationship_incremental_max_change: float = 5.0 # 会话中,关系值最大可变值
+ pfc_relationship_final_msg_count: int = 30 # 会话结束时,关系值判断最大可见上下文
+ pfc_relationship_final_default_change: float = 5.0 # 会话结束时,关系值默认更新值
+ pfc_relationship_final_max_change: float = 50.0 # 会话结束时,关系值最大可变值
+
+ # pfc.fallback
+ pfc_historical_fallback_exclude_seconds: int = 7200 # pfc 翻看聊天记录排除最近时长
+
+ # pfc.idle_chat
enable_idle_chat: bool = False # 是否启用 pfc 主动发言
idle_check_interval: int = 10 # 检查间隔,10分钟检查一次
min_cooldown: int = 7200 # 最短冷却时间,2小时 (7200秒)
@@ -301,6 +330,7 @@ class BotConfig:
nickname_queue_max_size: int = 100 # 绰号处理队列最大容量
nickname_process_sleep_interval: float = 5 # 绰号处理进程休眠间隔(秒)
nickname_analysis_history_limit: int = 30 # 绰号处理可见最大上下文
+ nickname_analysis_probability: float = 0.1 # 绰号随机概率命中,该值越大,绰号分析越频繁
# 模型配置
llm_reasoning: dict[str, str] = field(default_factory=lambda: {})
@@ -458,6 +488,9 @@ class BotConfig:
config.nickname_analysis_history_limit = group_nickname_config.get(
"nickname_analysis_history_limit", config.nickname_analysis_history_limit
)
+ config.nickname_analysis_probability = group_nickname_config.get(
+ "nickname_analysis_probability", config.nickname_analysis_probability
+ )
def bot(parent: dict):
# 机器人基础配置
@@ -547,10 +580,10 @@ class BotConfig:
"llm_heartflow",
"llm_PFC_action_planner",
"llm_PFC_chat",
- "llm_PFC_relationship_eval",
"llm_nickname_mapping",
"llm_scheduler_all",
"llm_scheduler_doing",
+ "llm_PFC_relationship_eval",
]
for item in config_list:
@@ -724,30 +757,97 @@ class BotConfig:
config.enable_friend_chat = experimental_config.get("enable_friend_chat", config.enable_friend_chat)
# config.enable_think_flow = experimental_config.get("enable_think_flow", config.enable_think_flow)
config.talk_allowed_private = set(str(user) for user in experimental_config.get("talk_allowed_private", []))
- if config.INNER_VERSION in SpecifierSet(">=1.1.0"):
- config.enable_pfc_chatting = experimental_config.get("pfc_chatting", config.enable_pfc_chatting)
+ if config.INNER_VERSION in SpecifierSet(">=1.6.2.4"):
+ config.enable_friend_whitelist = experimental_config.get(
+ "enable_friend_whitelist", config.enable_friend_whitelist
+ )
if config.INNER_VERSION in SpecifierSet(">=1.6.1.5"):
config.api_polling_max_retries = experimental_config.get(
"api_polling_max_retries", config.api_polling_max_retries
)
- if config.INNER_VERSION in SpecifierSet(">=1.6.2"):
- config.enable_pfc_reply_checker = experimental_config.get(
- "enable_pfc_reply_checker", config.enable_pfc_reply_checker
- )
- logger.info(f"PFC Reply Checker 状态: {'启用' if config.enable_pfc_reply_checker else '关闭'}")
- config.pfc_message_buffer_size = experimental_config.get(
- "pfc_message_buffer_size", config.pfc_message_buffer_size
- )
if config.INNER_VERSION in SpecifierSet(">=1.6.2.3"):
config.rename_person = experimental_config.get("rename_person", config.rename_person)
- def idle_chat(parent: dict):
- idle_chat_config = parent["idle_chat"]
- if config.INNER_VERSION in SpecifierSet(">=1.6.1.6"):
- config.enable_idle_chat = idle_chat_config.get("enable_idle_chat", config.enable_idle_chat)
- config.idle_check_interval = idle_chat_config.get("idle_check_interval", config.idle_check_interval)
- config.min_cooldown = idle_chat_config.get("min_cooldown", config.min_cooldown)
- config.max_cooldown = idle_chat_config.get("max_cooldown", config.max_cooldown)
+ def pfc(parent: dict):
+ if config.INNER_VERSION in SpecifierSet(">=1.6.2.4"):
+ pfc_config = parent.get("pfc", {})
+ # 解析 [pfc] 下的直接字段
+ config.enable_pfc_chatting = pfc_config.get("enable_pfc_chatting", config.enable_pfc_chatting)
+ config.pfc_message_buffer_size = pfc_config.get(
+ "pfc_message_buffer_size", config.pfc_message_buffer_size
+ )
+ config.pfc_recent_history_display_count = pfc_config.get(
+ "pfc_recent_history_display_count", config.pfc_recent_history_display_count
+ )
+
+ # 解析 [[pfc.checker]] 子表
+ checker_list = pfc_config.get("checker", [])
+ if checker_list and isinstance(checker_list, list):
+ checker_config = checker_list[0] if checker_list else {}
+ config.enable_pfc_reply_checker = checker_config.get(
+ "enable_pfc_reply_checker", config.enable_pfc_reply_checker
+ )
+ config.pfc_max_reply_attempts = checker_config.get(
+ "pfc_max_reply_attempts", config.pfc_max_reply_attempts
+ )
+ config.pfc_max_chat_history_for_checker = checker_config.get(
+ "pfc_max_chat_history_for_checker", config.pfc_max_chat_history_for_checker
+ )
+
+ # 解析 [[pfc.emotion]] 子表
+ emotion_list = pfc_config.get("emotion", [])
+ if emotion_list and isinstance(emotion_list, list):
+ emotion_config = emotion_list[0] if emotion_list else {}
+ config.pfc_emotion_update_intensity = emotion_config.get(
+ "pfc_emotion_update_intensity", config.pfc_emotion_update_intensity
+ )
+ config.pfc_emotion_history_count = emotion_config.get(
+ "pfc_emotion_history_count", config.pfc_emotion_history_count
+ )
+
+ # 解析 [[pfc.relationship]] 子表
+ relationship_list = pfc_config.get("relationship", [])
+ if relationship_list and isinstance(relationship_list, list):
+ relationship_config = relationship_list[0] if relationship_list else {}
+ config.pfc_relationship_incremental_interval = relationship_config.get(
+ "pfc_relationship_incremental_interval", config.pfc_relationship_incremental_interval
+ )
+ config.pfc_relationship_incremental_msg_count = relationship_config.get(
+ "pfc_relationship_incremental_msg_count", config.pfc_relationship_incremental_msg_count
+ )
+ config.pfc_relationship_incremental_default_change = relationship_config.get(
+ "pfc_relationship_incremental_default_change",
+ config.pfc_relationship_incremental_default_change,
+ )
+ config.pfc_relationship_incremental_max_change = relationship_config.get(
+ "pfc_relationship_incremental_max_change", config.pfc_relationship_incremental_max_change
+ )
+ config.pfc_relationship_final_msg_count = relationship_config.get(
+ "pfc_relationship_final_msg_count", config.pfc_relationship_final_msg_count
+ )
+ config.pfc_relationship_final_default_change = relationship_config.get(
+ "pfc_relationship_final_default_change", config.pfc_relationship_final_default_change
+ )
+ config.pfc_relationship_final_max_change = relationship_config.get(
+ "pfc_relationship_final_max_change", config.pfc_relationship_final_max_change
+ )
+
+ # 解析 [[pfc.fallback]] 子表
+ fallback_list = pfc_config.get("fallback", [])
+ if fallback_list and isinstance(fallback_list, list):
+ fallback_config = fallback_list[0] if fallback_list else {}
+ config.pfc_historical_fallback_exclude_seconds = fallback_config.get(
+ "pfc_historical_fallback_exclude_seconds", config.pfc_historical_fallback_exclude_seconds
+ )
+
+ # 解析 [[pfc.idle_chat]] 子表
+ idle_chat_list = pfc_config.get("idle_chat", [])
+ if idle_chat_list and isinstance(idle_chat_list, list):
+ idle_chat_config = idle_chat_list[0] if idle_chat_list else {}
+ config.enable_idle_chat = idle_chat_config.get("enable_idle_chat", config.enable_idle_chat)
+ config.idle_check_interval = idle_chat_config.get("idle_check_interval", config.idle_check_interval)
+ config.min_cooldown = idle_chat_config.get("min_cooldown", config.min_cooldown)
+ config.max_cooldown = idle_chat_config.get("max_cooldown", config.max_cooldown)
# 版本表达式:>=1.0.0,<2.0.0
# 允许字段:func: method, support: str, notice: str, necessary: bool
@@ -783,7 +883,7 @@ class BotConfig:
"normal_chat": {"func": normal_chat, "support": ">=1.6.0", "necessary": False},
"focus_chat": {"func": focus_chat, "support": ">=1.6.0", "necessary": False},
"group_nickname": {"func": group_nickname, "support": ">=1.6.1.1", "necessary": False},
- "idle_chat": {"func": idle_chat, "support": ">=1.6.1.6", "necessary": False},
+ "pfc": {"func": pfc, "support": ">=1.6.2.4", "necessary": False},
}
# 原地修改,将 字符串版本表达式 转换成 版本对象
diff --git a/src/do_tool/not_used/change_mood.py b/src/do_tool/not_used/change_mood.py
index 5dee6ac9..5d1e7f7a 100644
--- a/src/do_tool/not_used/change_mood.py
+++ b/src/do_tool/not_used/change_mood.py
@@ -1,10 +1,10 @@
-from src.do_tool.tool_can_use.base_tool import BaseTool
-from src.config.config import global_config
-from src.common.logger_manager import get_logger
-from src.plugins.moods.moods import MoodManager
-
from typing import Any
+from src.common.logger_manager import get_logger
+from src.config.config import global_config
+from src.do_tool.tool_can_use.base_tool import BaseTool
+from src.manager.mood_manager import mood_manager
+
logger = get_logger("change_mood_tool")
@@ -36,7 +36,6 @@ class ChangeMoodTool(BaseTool):
response_set = function_args.get("response_set")
_message_processed_plain_text = function_args.get("text")
- mood_manager = MoodManager.get_instance()
# gpt = ResponseGenerator()
if response_set is None:
diff --git a/src/heart_flow/chat_state_info.py b/src/heart_flow/chat_state_info.py
index 619f372f..bda5c26c 100644
--- a/src/heart_flow/chat_state_info.py
+++ b/src/heart_flow/chat_state_info.py
@@ -1,4 +1,4 @@
-from src.plugins.moods.moods import MoodManager
+from src.manager.mood_manager import mood_manager
import enum
@@ -13,5 +13,5 @@ class ChatStateInfo:
self.chat_status: ChatState = ChatState.ABSENT
self.current_state_time = 120
- self.mood_manager = MoodManager()
- self.mood = self.mood_manager.get_prompt()
+ self.mood_manager = mood_manager
+ self.mood = self.mood_manager.get_mood_prompt()
diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py
index d289a94a..3c6c19d6 100644
--- a/src/heart_flow/mai_state_manager.py
+++ b/src/heart_flow/mai_state_manager.py
@@ -3,7 +3,7 @@ import time
import random
from typing import List, Tuple, Optional
from src.common.logger_manager import get_logger
-from src.plugins.moods.moods import MoodManager
+from src.manager.mood_manager import mood_manager
from src.config.config import global_config
logger = get_logger("mai_state")
@@ -88,7 +88,7 @@ class MaiStateInfo:
self.last_min_check_time: float = time.time() # 上次1分钟规则检查时间
# Mood management is now part of MaiStateInfo
- self.mood_manager = MoodManager.get_instance() # Use singleton instance
+ self.mood_manager = mood_manager # Use singleton instance
def update_mai_status(self, new_status: MaiState) -> bool:
"""
@@ -124,7 +124,7 @@ class MaiStateInfo:
def get_mood_prompt(self) -> str:
"""获取当前的心情提示词"""
# Delegate to the internal mood manager
- return self.mood_manager.get_prompt()
+ return self.mood_manager.get_mood_prompt()
def get_current_state(self) -> MaiState:
"""获取当前的 MaiState"""
diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py
index 2d819a88..07221f42 100644
--- a/src/heart_flow/observation.py
+++ b/src/heart_flow/observation.py
@@ -95,7 +95,7 @@ class ChattingObservation(Observation):
if ids:
mid_memory_str = ""
for id in ids:
- print(f"id:{id}")
+ # print(f"id:{id}")
try:
for mid_memory in self.mid_memorys:
if mid_memory["id"] == id:
diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py
index 19af8528..d30de3c0 100644
--- a/src/heart_flow/sub_mind.py
+++ b/src/heart_flow/sub_mind.py
@@ -1,7 +1,10 @@
from .observation import ChattingObservation
+from src.plugins.knowledge.knowledge_lib import qa_manager
from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
+from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, build_readable_messages
import time
+import re
import traceback
from src.common.logger_manager import get_logger
from src.individuality.individuality import Individuality
@@ -24,18 +27,35 @@ logger = get_logger("sub_heartflow")
def init_prompt():
# --- Group Chat Prompt ---
group_prompt = """
-{extra_info}
-{relation_prompt}
-你的名字是{bot_name},{prompt_personality}
-{last_loop_prompt}
-{cycle_info_block}
-现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:
-{chat_observe_info}
+
+ 你的名字是{bot_name}。
+ {prompt_personality}
+
-你现在{mood_info}
-请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复或发言。然后思考你是否需要使用函数工具。
-思考并输出你的内心想法
-输出要求:
+
+ {extra_info}
+ {relation_prompt}
+
+
+
+ {last_loop_prompt}
+ {cycle_info_block}
+ 你现在{mood_info}
+
+
+
+ 现在是{time_now}。
+ 你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:
+{chat_observe_info}
+
+
+
+请仔细阅读当前聊天内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复或发言。然后思考你是否需要使用函数工具。
+思考并输出你真实的内心想法。
+
+
+
+
1. 根据聊天内容生成你的想法,{hf_do_next}
2. 不要分点、不要使用表情符号
3. 避免多余符号(冒号、引号、括号等)
@@ -43,11 +63,17 @@ def init_prompt():
5. 如果你刚发言,并且没有人回复你,请谨慎考虑要不要继续发消息
6. 不要把注意力放在别人发的表情包上,它们只是一种辅助表达方式
7. 注意分辨群里谁在跟谁说话,你不一定是当前聊天的主角,消息中的“你”不一定指的是你({bot_name}),也可能是别人
-8. 思考要不要回复或发言,如果要,需要思考一下具体说什么。
-工具使用说明:
+8. 思考要不要回复或发言,如果要,必须**明确写出**你准备发送的消息的具体内容是什么
+9. 默认使用中文
+
+
+
1. 输出想法后考虑是否需要使用工具
2. 工具可获取信息或执行操作
-3. 如需处理消息或回复,请使用工具。"""
+3. 如需处理消息或回复,请使用工具。
+
+
+"""
Prompt(group_prompt, "sub_heartflow_prompt_before")
# --- Private Chat Prompt ---
@@ -85,6 +111,35 @@ def init_prompt():
Prompt(last_loop_t, "last_loop")
+def parse_knowledge_and_get_max_relevance(knowledge_str: str) -> str | float:
+ """
+ 解析 qa_manager.get_knowledge 返回的字符串,提取所有知识的文本和最高的相关性得分。
+ 返回: (原始知识字符串, 最高相关性得分),如果无有效相关性则返回 (原始知识字符串, 0.0)
+ """
+ if not knowledge_str:
+ return None, 0.0
+
+ max_relevance = 0.0
+ # 正则表达式匹配 "该条知识对于问题的相关性:数字"
+ # 我们需要捕获数字部分
+ relevance_scores = re.findall(r"该条知识对于问题的相关性:([0-9.]+)", knowledge_str)
+
+ if relevance_scores:
+ try:
+ max_relevance = max(float(score) for score in relevance_scores)
+ except ValueError:
+ logger.warning(f"解析相关性得分时出错: {relevance_scores}")
+ return knowledge_str, 0.0 # 出错时返回0.0
+ else:
+ # 如果没有找到 "该条知识对于问题的相关性:" 这样的模式,
+ # 说明可能 qa_manager 返回的格式有变,或者没有有效的知识。
+ # 在这种情况下,我们无法确定相关性,保守起见返回0.0
+ logger.debug(f"在知识字符串中未找到明确的相关性得分标记: '{knowledge_str[:100]}...'")
+ return knowledge_str, 0.0
+
+ return knowledge_str, max_relevance
+
+
def calculate_similarity(text_a: str, text_b: str) -> float:
"""
计算两个文本字符串的相似度。
@@ -127,7 +182,7 @@ class SubMind:
self.llm_model = LLMRequest(
model=global_config.llm_sub_heartflow,
temperature=global_config.llm_sub_heartflow["temp"],
- max_tokens=800,
+ max_tokens=1000,
request_type="sub_heart_flow",
)
@@ -142,6 +197,14 @@ class SubMind:
name = chat_manager.get_stream_name(self.subheartflow_id)
self.log_prefix = f"[{name}] "
self._update_structured_info_str()
+ # 阶梯式筛选
+ self.knowledge_retrieval_steps = self.knowledge_retrieval_steps = [
+ {"name": "latest_1_msg", "limit": 1, "relevance_threshold": 0.75}, # 新增:最新1条,极高阈值
+ {"name": "latest_2_msgs", "limit": 2, "relevance_threshold": 0.65}, # 新增:最新2条,较高阈值
+ {"name": "short_window_3_msgs", "limit": 3, "relevance_threshold": 0.50}, # 原有的3条,阈值可保持或微调
+ {"name": "medium_window_8_msgs", "limit": 8, "relevance_threshold": 0.30}, # 原有的8条,阈值可保持或微调
+ # 完整窗口的回退逻辑保持不变
+ ]
def _update_structured_info_str(self):
"""根据 structured_info 更新 structured_info_str"""
@@ -184,23 +247,26 @@ class SubMind:
# ---------- 0. 更新和清理 structured_info ----------
if self.structured_info:
logger.debug(
- f"{self.log_prefix} 更新前的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}"
+ f"{self.log_prefix} 清理前 structured_info 中包含的lpmm_knowledge数量: "
+ f"{len([item for item in self.structured_info if item.get('type') == 'lpmm_knowledge'])}"
)
- updated_info = []
- for item in self.structured_info:
+ # 筛选出所有不是 lpmm_knowledge 类型的条目,或者其他需要保留的条目
+ info_to_keep = [item for item in self.structured_info if item.get("type") != "lpmm_knowledge"]
+
+ # 针对我们仅希望 lpmm_knowledge "用完即弃" 的情况:
+ processed_info_to_keep = []
+ for item in info_to_keep: # info_to_keep 已经不包含 lpmm_knowledge
item["ttl"] -= 1
if item["ttl"] > 0:
- updated_info.append(item)
+ processed_info_to_keep.append(item)
else:
- logger.debug(f"{self.log_prefix} 移除过期的 structured_info 项: {item['id']}")
- self.structured_info = updated_info
+ logger.debug(f"{self.log_prefix} 移除过期的非lpmm_knowledge项: {item.get('id', '未知ID')}")
+
+ self.structured_info = processed_info_to_keep
logger.debug(
- f"{self.log_prefix} 更新后的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}"
+ f"{self.log_prefix} 清理后 structured_info (仅保留非lpmm_knowledge且TTL有效项): "
+ f"{safe_json_dumps(self.structured_info, ensure_ascii=False)}"
)
- self._update_structured_info_str()
- logger.debug(
- f"{self.log_prefix} 当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}"
- )
# ---------- 1. 准备基础数据 ----------
# 获取现有想法和情绪状态
@@ -270,6 +336,117 @@ class SubMind:
logger.error(f"{self.log_prefix} 获取记忆时出错: {e}")
logger.error(traceback.format_exc())
+ # ---------- 2.5 阶梯式获取知识库信息 ----------
+ final_knowledge_to_add = None
+ retrieval_source_info = "未进行知识检索"
+
+ # 确保 observation 对象存在且可用
+ if not observation:
+ logger.warning(f"{self.log_prefix} Observation 对象不可用,跳过知识库检索。")
+ else:
+ # 阶段1和阶段2的阶梯检索
+ for step_config in self.knowledge_retrieval_steps:
+ step_name = step_config["name"]
+ limit = step_config["limit"]
+ threshold = step_config["relevance_threshold"]
+
+ logger.info(f"{self.log_prefix} 尝试阶梯检索 - 阶段: {step_name} (最近{limit}条, 阈值>{threshold})")
+
+ try:
+ # 1. 获取当前阶段的聊天记录上下文
+ # 我们需要从 observation 中获取原始消息列表来构建特定长度的上下文
+ # get_raw_msg_before_timestamp_with_chat 在 observation.py 中被导入
+ # from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, build_readable_messages
+
+ # 需要确保 ChattingObservation 的实例 (self.observations[0]) 能提供 chat_id
+ # 并且 build_readable_messages 可用
+ context_messages_dicts = get_raw_msg_before_timestamp_with_chat(
+ chat_id=observation.chat_id, timestamp=time.time(), limit=limit
+ )
+
+ if not context_messages_dicts:
+ logger.debug(f"{self.log_prefix} 阶段 '{step_name}' 未获取到聊天记录,跳过此阶段。")
+ continue
+
+ current_context_text = await build_readable_messages(
+ messages=context_messages_dicts,
+ timestamp_mode="lite", # 或者您认为适合知识检索的模式
+ )
+
+ if not current_context_text:
+ logger.debug(f"{self.log_prefix} 阶段 '{step_name}' 构建的上下文为空,跳过此阶段。")
+ continue
+
+ logger.debug(f"{self.log_prefix} 阶段 '{step_name}' 使用上下文: '{current_context_text[:150]}...'")
+
+ # 2. 调用知识库进行检索
+ raw_knowledge_str = qa_manager.get_knowledge(current_context_text)
+
+ if raw_knowledge_str:
+ # 3. 解析知识并检查相关性
+ knowledge_content, max_relevance = parse_knowledge_and_get_max_relevance(raw_knowledge_str)
+ logger.info(f"{self.log_prefix} 阶段 '{step_name}' 检索到知识,最高相关性: {max_relevance:.4f}")
+
+ if max_relevance >= threshold:
+ logger.info(
+ f"{self.log_prefix} 阶段 '{step_name}' 满足阈值 ({max_relevance:.4f} >= {threshold}),采纳此知识。"
+ )
+ final_knowledge_to_add = knowledge_content
+ retrieval_source_info = f"阶段 '{step_name}' (最近{limit}条, 相关性 {max_relevance:.4f})"
+ break # 找到符合条件的知识,跳出阶梯循环
+ else:
+ logger.info(
+ f"{self.log_prefix} 阶段 '{step_name}' 未满足阈值 ({max_relevance:.4f} < {threshold}),继续下一阶段。"
+ )
+ else:
+ logger.debug(f"{self.log_prefix} 阶段 '{step_name}' 未从知识库检索到任何内容。")
+
+ except Exception as e_step:
+ logger.error(f"{self.log_prefix} 阶梯检索阶段 '{step_name}' 发生错误: {e_step}")
+ logger.error(traceback.format_exc())
+ continue # 当前阶段出错,尝试下一阶段
+
+ # 阶段3: 如果前面的阶梯都没有成功,则使用完整的 chat_observe_info (即您配置的20条)
+ if not final_knowledge_to_add and chat_observe_info: # 确保 chat_observe_info 可用
+ logger.info(
+ f"{self.log_prefix} 前序阶梯均未满足条件,尝试使用完整观察窗口 ('{observation.max_now_obs_len}'条)进行检索。"
+ )
+ try:
+ raw_knowledge_str = qa_manager.get_knowledge(chat_observe_info)
+ if raw_knowledge_str:
+ # 对于完整窗口,我们可能不强制要求阈值,或者使用一个较低的阈值
+ # 或者,您可以选择在这里仍然应用一个阈值,例如 self.knowledge_retrieval_steps 中最后一个的阈值,或一个特定值
+ knowledge_content, max_relevance = parse_knowledge_and_get_max_relevance(raw_knowledge_str)
+ logger.info(
+ f"{self.log_prefix} 完整窗口检索到知识,(此处未设阈值,或相关性: {max_relevance:.4f})。"
+ )
+ final_knowledge_to_add = knowledge_content # 默认采纳
+ retrieval_source_info = (
+ f"完整窗口 (最多{observation.max_now_obs_len}条, 相关性 {max_relevance:.4f})"
+ )
+ else:
+ logger.debug(f"{self.log_prefix} 完整窗口检索也未找到知识。")
+ except Exception as e_full:
+ logger.error(f"{self.log_prefix} 完整窗口知识检索发生错误: {e_full}")
+ logger.error(traceback.format_exc())
+
+ # 将最终选定的知识(如果有)添加到 structured_info
+ if final_knowledge_to_add:
+ knowledge_item = {
+ "type": "lpmm_knowledge",
+ "id": f"lpmm_knowledge_{time.time()}",
+ "content": final_knowledge_to_add,
+ "ttl": 1, # 由于是当轮精心选择的,可以让TTL短一些,下次重新评估(或者按照您的意愿设为3)
+ }
+ # 我们在方法开头已经清理了旧的 lpmm_knowledge,这里直接添加新的
+ self.structured_info.append(knowledge_item)
+ logger.info(
+ f"{self.log_prefix} 添加了来自 '{retrieval_source_info}' 的知识到 structured_info (ID: {knowledge_item['id']})"
+ )
+ self._update_structured_info_str() # 更新字符串表示
+ else:
+ logger.info(f"{self.log_prefix} 经过所有阶梯检索后,没有最终采纳的知识。")
+
# ---------- 3. 准备工具和个性化数据 ----------
# 初始化工具
tool_instance = ToolUser()
@@ -300,12 +477,16 @@ class SubMind:
# 思考指导选项和权重
hf_options = [
(
- "可以参考之前的想法,在原来想法的基础上继续思考,但是也要注意话题的推进,不要在一个话题上停留太久,除非你觉得真的有必要",
+ "可以参考之前的想法,在原来想法的基础上继续思考,但是也要注意话题的推进,**不要在一个话题上停留太久或揪着一个话题不放,除非你觉得真的有必要**",
0.3,
),
- ("可以参考之前的想法,在原来的想法上尝试新的话题", 0.3),
- ("不要太深入,注意话题的推进,不要在一个话题上停留太久,除非你觉得真的有必要", 0.2),
- ("进行深入思考,但是注意话题的推进,不要在一个话题上停留太久,除非你觉得真的有必要", 0.2),
+ ("可以参考之前的想法,在原来的想法上**尝试新的话题**", 0.3),
+ ("不要太深入,注意话题的推进,**不要在一个话题上停留太久或揪着一个话题不放,除非你觉得真的有必要**", 0.2),
+ (
+ "进行深入思考,但是注意话题的推进,**不要在一个话题上停留太久或揪着一个话题不放,除非你觉得真的有必要**",
+ 0.2,
+ ),
+ ("可以参考之前的想法继续思考,并结合你自身的人设,知识,信息,回忆等等", 0.08),
]
last_cycle = history_cycle[-1] if history_cycle else None
diff --git a/src/main.py b/src/main.py
index be71524e..09570a4f 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,7 +1,12 @@
import asyncio
import time
-from .plugins.utils.statistic import LLMStatistics
-from .plugins.moods.moods import MoodManager
+
+from maim_message import MessageServer
+
+from .plugins.remote.remote import TelemetryHeartBeatTask
+from .manager.async_task_manager import async_task_manager
+from .plugins.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask
+from .manager.mood_manager import MoodPrintTask, MoodUpdateTask
from .plugins.schedule.schedule_generator import bot_schedule
from .plugins.emoji_system.emoji_manager import emoji_manager
from .plugins.person_info.person_info import person_info_manager
@@ -14,9 +19,8 @@ from .plugins.storage.storage import MessageStorage
from .config.config import global_config
from .plugins.chat.bot import chat_bot
from .common.logger_manager import get_logger
-from .plugins.remote import heartbeat_thread # noqa: F401
from .individuality.individuality import Individuality
-from .common.server import global_server
+from .common.server import global_server, Server
from rich.traceback import install
from .api.main import start_api_server
@@ -27,17 +31,14 @@ logger = get_logger("main")
class MainSystem:
def __init__(self):
- self.llm_stats = LLMStatistics("llm_statistics.txt")
- self.mood_manager = MoodManager.get_instance()
- self.hippocampus_manager = HippocampusManager.get_instance()
- self._message_manager_started = False
- self.individuality = Individuality.get_instance()
+ self.hippocampus_manager: HippocampusManager = HippocampusManager.get_instance()
+ self.individuality: Individuality = Individuality.get_instance()
# 使用消息API替代直接的FastAPI实例
from .plugins.message import global_api
- self.app = global_api
- self.server = global_server
+ self.app: MessageServer = global_api
+ self.server: Server = global_server
async def initialize(self):
"""初始化系统组件"""
@@ -51,9 +52,15 @@ class MainSystem:
async def _init_components(self):
"""初始化其他组件"""
init_start_time = time.time()
- # 启动LLM统计
- self.llm_stats.start()
- logger.success("LLM统计功能启动成功")
+
+ # 添加在线时间统计任务
+ await async_task_manager.add_task(OnlineTimeRecordTask())
+
+ # 添加统计信息输出任务
+ await async_task_manager.add_task(StatisticOutputTask())
+
+ # 添加遥测心跳任务
+ await async_task_manager.add_task(TelemetryHeartBeatTask())
# 启动API服务器
start_api_server()
@@ -62,9 +69,10 @@ class MainSystem:
emoji_manager.initialize()
logger.success("表情包管理器初始化成功")
- # 启动情绪管理器
- self.mood_manager.start_mood_update(update_interval=global_config.mood_update_interval)
- logger.success("情绪管理器启动成功")
+ # 添加情绪衰减任务
+ await async_task_manager.add_task(MoodUpdateTask())
+ # 添加情绪打印任务
+ await async_task_manager.add_task(MoodPrintTask())
# 检查并清除person_info冗余字段,启动个人习惯推断
await person_info_manager.del_all_undefined_field()
@@ -129,7 +137,6 @@ class MainSystem:
self.build_memory_task(),
self.forget_memory_task(),
self.consolidate_memory_task(),
- self.print_mood_task(),
self.remove_recalled_message_task(),
emoji_manager.start_periodic_check_register(),
self.app.run(),
@@ -163,12 +170,6 @@ class MainSystem:
await HippocampusManager.get_instance().consolidate_memory()
print("\033[1;32m[记忆整合]\033[0m 记忆整合完成")
- async def print_mood_task(self):
- """打印情绪状态"""
- while True:
- self.mood_manager.print_mood_status()
- await asyncio.sleep(60)
-
@staticmethod
async def remove_recalled_message_task():
"""删除撤回消息任务"""
diff --git a/src/manager/async_task_manager.py b/src/manager/async_task_manager.py
new file mode 100644
index 00000000..720e918a
--- /dev/null
+++ b/src/manager/async_task_manager.py
@@ -0,0 +1,150 @@
+from abc import abstractmethod
+
+import asyncio
+from asyncio import Task, Event, Lock
+from typing import Callable, Dict
+
+from src.common.logger_manager import get_logger
+
+logger = get_logger("async_task_manager")
+
+
+class AsyncTask:
+ """异步任务基类"""
+
+ def __init__(self, task_name: str | None = None, wait_before_start: int = 0, run_interval: int = 0):
+ self.task_name: str = task_name or self.__class__.__name__
+ """任务名称"""
+
+ self.wait_before_start: int = wait_before_start
+ """运行任务前是否进行等待(单位:秒,设为0则不等待)"""
+
+ self.run_interval: int = run_interval
+ """多次运行的时间间隔(单位:秒,设为0则仅运行一次)"""
+
+ @abstractmethod
+ async def run(self):
+ """
+ 任务的执行过程
+ """
+ pass
+
+ async def start_task(self, abort_flag: asyncio.Event):
+ if self.wait_before_start > 0:
+ # 等待指定时间后开始任务
+ await asyncio.sleep(self.wait_before_start)
+
+ while not abort_flag.is_set():
+ await self.run()
+ if self.run_interval > 0:
+ await asyncio.sleep(self.run_interval)
+ else:
+ break
+
+
+class AsyncTaskManager:
+ """异步任务管理器"""
+
+ def __init__(self):
+ self.tasks: Dict[str, Task] = {}
+ """任务列表"""
+
+ self.abort_flag: Event = Event()
+ """是否中止任务标志"""
+
+ self._lock: Lock = Lock()
+ """异步锁,当可能出现await时需要加锁"""
+
+ def _remove_task_call_back(self, task: Task):
+ """
+ call_back: 任务完成后移除任务
+ """
+ task_name = task.get_name()
+ if task_name in self.tasks:
+ # 任务完成后移除任务
+ del self.tasks[task_name]
+ logger.debug(f"已移除任务 '{task_name}'")
+ else:
+ logger.warning(f"尝试移除不存在的任务 '{task_name}'")
+
+ @staticmethod
+ def _default_finish_call_back(task: Task):
+ """
+ call_back: 默认的任务完成回调函数
+ """
+ try:
+ task.result()
+ logger.debug(f"任务 '{task.get_name()}' 完成")
+ except asyncio.CancelledError:
+ logger.debug(f"任务 '{task.get_name()}' 被取消")
+ except Exception as e:
+ logger.error(f"任务 '{task.get_name()}' 执行时发生异常: {e}", exc_info=True)
+
+ async def add_task(self, task: AsyncTask, call_back: Callable[[asyncio.Task], None] | None = None):
+ """
+ 添加任务
+ """
+ if not issubclass(task.__class__, AsyncTask):
+ raise TypeError(f"task '{task.__class__.__name__}' 必须是继承 AsyncTask 的子类")
+
+ async with self._lock: # 由于可能需要await等待任务完成,所以需要加异步锁
+ if task.task_name in self.tasks:
+ logger.warning(f"已存在名称为 '{task.task_name}' 的任务,正在尝试取消并替换")
+ self.tasks[task.task_name].cancel() # 取消已存在的任务
+ await self.tasks[task.task_name] # 等待任务完成
+ logger.info(f"成功结束任务 '{task.task_name}'")
+
+ # 创建新任务
+ task_inst = asyncio.create_task(task.start_task(self.abort_flag))
+ task_inst.set_name(task.task_name)
+ task_inst.add_done_callback(self._remove_task_call_back) # 添加完成回调函数-完成任务后自动移除任务
+ task_inst.add_done_callback(
+ call_back or self._default_finish_call_back
+ ) # 添加完成回调函数-用户自定义,或默认的FallBack
+
+ self.tasks[task.task_name] = task_inst # 将任务添加到任务列表
+ logger.info(f"已启动任务 '{task.task_name}'")
+
+ def get_tasks_status(self) -> Dict[str, Dict[str, str]]:
+ """
+ 获取所有任务的状态
+ """
+ tasks_status = {}
+ for task_name, task in self.tasks.items():
+ tasks_status[task_name] = {
+ "status": "running" if not task.done() else "done",
+ }
+ return tasks_status
+
+ async def stop_and_wait_all_tasks(self):
+ """
+ 终止所有任务并等待它们完成(该方法会阻塞其它尝试add_task()的操作)
+ """
+ async with self._lock: # 由于可能需要await等待任务完成,所以需要加异步锁
+ # 设置中止标志
+ self.abort_flag.set()
+ # 取消所有任务
+ for name, inst in self.tasks.items():
+ try:
+ inst.cancel()
+ except asyncio.CancelledError:
+ logger.info(f"已取消任务 '{name}'")
+
+ # 等待所有任务完成
+ for task_name, task_inst in self.tasks.items():
+ if not task_inst.done():
+ try:
+ await task_inst
+ except asyncio.CancelledError: # 此处再次捕获取消异常,防止stop_all_tasks()时延迟抛出异常
+ logger.info(f"任务 {task_name} 已取消")
+ except Exception as e:
+ logger.error(f"任务 {task_name} 执行时发生异常: {e}", ext_info=True)
+
+ # 清空任务列表
+ self.tasks.clear()
+ self.abort_flag.clear()
+ logger.info("所有异步任务已停止")
+
+
+async_task_manager = AsyncTaskManager()
+"""全局异步任务管理器实例"""
diff --git a/src/manager/local_store_manager.py b/src/manager/local_store_manager.py
new file mode 100644
index 00000000..f172d889
--- /dev/null
+++ b/src/manager/local_store_manager.py
@@ -0,0 +1,67 @@
+import json
+import os
+
+from src.common.logger_manager import get_logger
+
+LOCAL_STORE_FILE_PATH = "data/local_store.json"
+
+logger = get_logger("local_storage")
+
+
+class LocalStoreManager:
+ file_path: str
+ """本地存储路径"""
+
+ store: dict[str, str | list | dict | int | float | bool]
+ """本地存储数据"""
+
+ def __init__(self, local_store_path: str | None = None):
+ self.file_path = local_store_path or LOCAL_STORE_FILE_PATH
+ self.store = {}
+ self.load_local_store()
+
+ def __getitem__(self, item: str) -> str | list | dict | int | float | bool | None:
+ """获取本地存储数据"""
+ return self.store.get(item, None)
+
+ def __setitem__(self, key: str, value: str | list | dict | int | float | bool):
+ """设置本地存储数据"""
+ self.store[key] = value
+ self.save_local_store()
+
+ def __contains__(self, item: str) -> bool:
+ """检查本地存储数据是否存在"""
+ return item in self.store
+
+ def load_local_store(self):
+ """加载本地存储数据"""
+ if os.path.exists(self.file_path):
+ # 存在本地存储文件,加载数据
+ logger.info("正在阅读记事本......我在看,我真的在看!")
+ logger.debug(f"加载本地存储数据: {self.file_path}")
+ try:
+ with open(self.file_path, "r", encoding="utf-8") as f:
+ self.store = json.load(f)
+ logger.success("全都记起来了!")
+ except json.JSONDecodeError:
+ logger.warning("啊咧?记事本被弄脏了,正在重建记事本......")
+ self.store = {}
+ with open(self.file_path, "w", encoding="utf-8") as f:
+ json.dump({}, f, ensure_ascii=False, indent=4)
+ logger.success("记事本重建成功!")
+ else:
+ # 不存在本地存储文件,创建新的目录和文件
+ logger.warning("啊咧?记事本不存在,正在创建新的记事本......")
+ os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
+ with open(self.file_path, "w", encoding="utf-8") as f:
+ json.dump({}, f, ensure_ascii=False, indent=4)
+ logger.success("记事本创建成功!")
+
+ def save_local_store(self):
+ """保存本地存储数据"""
+ logger.debug(f"保存本地存储数据: {self.file_path}")
+ with open(self.file_path, "w", encoding="utf-8") as f:
+ json.dump(self.store, f, ensure_ascii=False, indent=4)
+
+
+local_storage = LocalStoreManager("data/local_store.json") # 全局单例化
diff --git a/src/manager/mood_manager.py b/src/manager/mood_manager.py
new file mode 100644
index 00000000..42677d4e
--- /dev/null
+++ b/src/manager/mood_manager.py
@@ -0,0 +1,296 @@
+import asyncio
+import math
+import time
+from dataclasses import dataclass
+from typing import Dict, Tuple
+
+from ..config.config import global_config
+from ..common.logger_manager import get_logger
+from ..manager.async_task_manager import AsyncTask
+from ..individuality.individuality import Individuality
+
+logger = get_logger("mood")
+
+
+@dataclass
+class MoodState:
+ valence: float
+ """愉悦度 (-1.0 到 1.0),-1表示极度负面,1表示极度正面"""
+ arousal: float
+ """唤醒度 (-1.0 到 1.0),-1表示抑制,1表示兴奋"""
+ text: str
+ """心情的文本描述"""
+
+
+@dataclass
+class MoodChangeHistory:
+ valence_direction_factor: int
+ """愉悦度变化的系数(正为增益,负为抑制)"""
+ arousal_direction_factor: int
+ """唤醒度变化的系数(正为增益,负为抑制)"""
+
+
+class MoodUpdateTask(AsyncTask):
+ def __init__(self):
+ super().__init__(
+ task_name="Mood Update Task",
+ wait_before_start=global_config.mood_update_interval,
+ run_interval=global_config.mood_update_interval,
+ )
+
+ # 从配置文件获取衰减率
+ self.decay_rate_valence: float = 1 - global_config.mood_decay_rate
+ """愉悦度衰减率"""
+ self.decay_rate_arousal: float = 1 - global_config.mood_decay_rate
+ """唤醒度衰减率"""
+
+ self.last_update = time.time()
+ """上次更新时间"""
+
+ async def run(self):
+ current_time = time.time()
+ time_diff = current_time - self.last_update
+ agreeableness_factor = 1 # 宜人性系数
+ agreeableness_bias = 0 # 宜人性偏置
+ neuroticism_factor = 0.5 # 神经质系数
+ # 获取人格特质
+ personality = Individuality.get_instance().personality
+ if personality:
+ # 神经质:影响情绪变化速度
+ neuroticism_factor = 1 + (personality.neuroticism - 0.5) * 0.4
+ agreeableness_factor = 1 + (personality.agreeableness - 0.5) * 0.4
+
+ # 宜人性:影响情绪基准线
+ if personality.agreeableness < 0.2:
+ agreeableness_bias = (personality.agreeableness - 0.2) * 0.5
+ elif personality.agreeableness > 0.8:
+ agreeableness_bias = (personality.agreeableness - 0.8) * 0.5
+ else:
+ agreeableness_bias = 0
+
+ # 分别计算正向和负向的衰减率
+ if mood_manager.current_mood.valence >= 0:
+ # 正向情绪衰减
+ decay_rate_positive = self.decay_rate_valence * (1 / agreeableness_factor)
+ valence_target = 0 + agreeableness_bias
+ new_valence = valence_target + (mood_manager.current_mood.valence - valence_target) * math.exp(
+ -decay_rate_positive * time_diff * neuroticism_factor
+ )
+ else:
+ # 负向情绪衰减
+ decay_rate_negative = self.decay_rate_valence * agreeableness_factor
+ valence_target = 0 + agreeableness_bias
+ new_valence = valence_target + (mood_manager.current_mood.valence - valence_target) * math.exp(
+ -decay_rate_negative * time_diff * neuroticism_factor
+ )
+
+ # Arousal 向中性(0)回归
+ arousal_target = 0
+ new_arousal = arousal_target + (mood_manager.current_mood.arousal - arousal_target) * math.exp(
+ -self.decay_rate_arousal * time_diff * neuroticism_factor
+ )
+
+ mood_manager.set_current_mood(new_valence, new_arousal)
+
+ self.last_update = current_time
+
+
+class MoodPrintTask(AsyncTask):
+ def __init__(self):
+ super().__init__(
+ task_name="Mood Print Task",
+ wait_before_start=60,
+ run_interval=60,
+ )
+
+ async def run(self):
+ # 打印当前心情
+ logger.info(
+ f"愉悦度: {mood_manager.current_mood.valence:.2f}, "
+ f"唤醒度: {mood_manager.current_mood.arousal:.2f}, "
+ f"心情: {mood_manager.current_mood.text}"
+ )
+
+
+class MoodManager:
+ # TODO: 改进,使用具有实验支持的新情绪模型
+
+ EMOTION_FACTOR_MAP: Dict[str, Tuple[float, float]] = {
+ "开心": (0.21, 0.6),
+ "害羞": (0.15, 0.2),
+ "愤怒": (-0.24, 0.8),
+ "恐惧": (-0.21, 0.7),
+ "悲伤": (-0.21, 0.3),
+ "厌恶": (-0.12, 0.4),
+ "惊讶": (0.06, 0.7),
+ "困惑": (0.0, 0.6),
+ "平静": (0.03, 0.5),
+ }
+ """
+ 情绪词映射表 {mood: (valence, arousal)}
+ 将情绪描述词映射到愉悦度和唤醒度的元组
+ """
+
+ EMOTION_POINT_MAP: Dict[Tuple[float, float], str] = {
+ # 第一象限:高唤醒,正愉悦
+ (0.5, 0.4): "兴奋",
+ (0.3, 0.6): "快乐",
+ (0.2, 0.3): "满足",
+ # 第二象限:高唤醒,负愉悦
+ (-0.5, 0.4): "愤怒",
+ (-0.3, 0.6): "焦虑",
+ (-0.2, 0.3): "烦躁",
+ # 第三象限:低唤醒,负愉悦
+ (-0.5, -0.4): "悲伤",
+ (-0.3, -0.3): "疲倦",
+ (-0.4, -0.7): "疲倦",
+ # 第四象限:低唤醒,正愉悦
+ (0.2, -0.1): "平静",
+ (0.3, -0.2): "安宁",
+ (0.5, -0.4): "放松",
+ }
+ """
+ 情绪文本映射表 {(valence, arousal): mood}
+ 将量化的情绪状态元组映射到文本描述
+ """
+
+ def __init__(self):
+ self.current_mood = MoodState(
+ valence=0.0,
+ arousal=0.0,
+ text="平静",
+ )
+ """当前情绪状态"""
+
+ self.mood_change_history: MoodChangeHistory = MoodChangeHistory(
+ valence_direction_factor=0,
+ arousal_direction_factor=0,
+ )
+ """情绪变化历史"""
+
+ self._lock = asyncio.Lock()
+ """异步锁,用于保护线程安全"""
+
+ def set_current_mood(self, new_valence: float, new_arousal: float):
+ """
+ 设置当前情绪状态
+ :param new_valence: 新的愉悦度
+ :param new_arousal: 新的唤醒度
+ """
+ # 限制范围
+ self.current_mood.valence = max(-1.0, min(new_valence, 1.0))
+ self.current_mood.arousal = max(-1.0, min(new_arousal, 1.0))
+
+ closest_mood = None
+ min_distance = float("inf")
+
+ for (v, a), text in self.EMOTION_POINT_MAP.items():
+ # 计算当前情绪状态与每个情绪文本的欧氏距离
+ distance = math.sqrt((self.current_mood.valence - v) ** 2 + (self.current_mood.arousal - a) ** 2)
+ if distance < min_distance:
+ min_distance = distance
+ closest_mood = text
+
+ if closest_mood:
+ self.current_mood.text = closest_mood
+
+ def update_current_mood(self, valence_delta: float, arousal_delta: float):
+ """
+ 根据愉悦度和唤醒度变化量更新当前情绪状态
+ :param valence_delta: 愉悦度变化量
+ :param arousal_delta: 唤醒度变化量
+ """
+ # 计算连续增益/抑制
+ # 规则:多次相同方向的变化会有更大的影响系数,反方向的变化会清零影响系数(系数的正负号由变化方向决定)
+ if valence_delta * self.mood_change_history.valence_direction_factor > 0:
+ # 如果方向相同,则根据变化方向改变系数
+ if valence_delta > 0:
+ self.mood_change_history.valence_direction_factor += 1 # 若为正向,则增加
+ else:
+ self.mood_change_history.valence_direction_factor -= 1 # 若为负向,则减少
+ else:
+ # 如果方向不同,则重置计数
+ self.mood_change_history.valence_direction_factor = 0
+
+ if arousal_delta * self.mood_change_history.arousal_direction_factor > 0:
+ # 如果方向相同,则根据变化方向改变系数
+ if arousal_delta > 0:
+ self.mood_change_history.arousal_direction_factor += 1 # 若为正向,则增加计数
+ else:
+ self.mood_change_history.arousal_direction_factor -= 1 # 若为负向,则减少计数
+ else:
+ # 如果方向不同,则重置计数
+ self.mood_change_history.arousal_direction_factor = 0
+
+ # 计算增益/抑制的结果
+ # 规则:如果当前情绪状态与变化方向相同,则增益;否则抑制
+ if self.current_mood.valence * self.mood_change_history.valence_direction_factor > 0:
+ valence_delta = valence_delta * (1.01 ** abs(self.mood_change_history.valence_direction_factor))
+ else:
+ valence_delta = valence_delta * (0.99 ** abs(self.mood_change_history.valence_direction_factor))
+
+ if self.current_mood.arousal * self.mood_change_history.arousal_direction_factor > 0:
+ arousal_delta = arousal_delta * (1.01 ** abs(self.mood_change_history.arousal_direction_factor))
+ else:
+ arousal_delta = arousal_delta * (0.99 ** abs(self.mood_change_history.arousal_direction_factor))
+
+ self.set_current_mood(
+ new_valence=self.current_mood.valence + valence_delta,
+ new_arousal=self.current_mood.arousal + arousal_delta,
+ )
+
+ def get_mood_prompt(self) -> str:
+ """
+ 根据当前情绪状态生成提示词
+ """
+ base_prompt = f"当前心情:{self.current_mood.text}。"
+
+ # 根据情绪状态添加额外的提示信息
+ if self.current_mood.valence > 0.5:
+ base_prompt += "你现在心情很好,"
+ elif self.current_mood.valence < -0.5:
+ base_prompt += "你现在心情不太好,"
+
+ if self.current_mood.arousal > 0.4:
+ base_prompt += "情绪比较激动。"
+ elif self.current_mood.arousal < -0.4:
+ base_prompt += "情绪比较平静。"
+
+ return base_prompt
+
+ def get_arousal_multiplier(self) -> float:
+ """
+ 根据当前情绪状态返回唤醒度乘数
+ """
+ if self.current_mood.arousal > 0.4:
+ multiplier = 1 + min(0.15, (self.current_mood.arousal - 0.4) / 3)
+ return multiplier
+ elif self.current_mood.arousal < -0.4:
+ multiplier = 1 - min(0.15, ((0 - self.current_mood.arousal) - 0.4) / 3)
+ return multiplier
+ return 1.0
+
+ def update_mood_from_emotion(self, emotion: str, intensity: float = 1.0) -> None:
+ """
+ 根据情绪词更新心情状态
+ :param emotion: 情绪词(如'开心', '悲伤'等位于self.EMOTION_FACTOR_MAP中的键)
+ :param intensity: 情绪强度(0.0-1.0)
+ """
+ if emotion not in self.EMOTION_FACTOR_MAP:
+ logger.error(f"[情绪更新] 未知情绪词: {emotion}")
+ return
+
+ valence_change, arousal_change = self.EMOTION_FACTOR_MAP[emotion]
+ old_valence = self.current_mood.valence
+ old_arousal = self.current_mood.arousal
+ old_mood = self.current_mood.text
+
+ self.update_current_mood(valence_change, arousal_change) # 更新当前情绪状态
+
+ logger.info(
+ f"[情绪变化] {emotion}(强度:{intensity:.2f}) | 愉悦度:{old_valence:.2f}->{self.current_mood.valence:.2f}, 唤醒度:{old_arousal:.2f}->{self.current_mood.arousal:.2f} | 心情:{old_mood}->{self.current_mood.text}"
+ )
+
+
+mood_manager = MoodManager()
+"""全局情绪管理器"""
diff --git a/src/plugins/PFC/PFC_idle/idle_chat.py b/src/plugins/PFC/PFC_idle/idle_chat.py
index 131738df..f4187b89 100644
--- a/src/plugins/PFC/PFC_idle/idle_chat.py
+++ b/src/plugins/PFC/PFC_idle/idle_chat.py
@@ -1,3 +1,5 @@
+# TODO: 开机自启,遍历所有可发起的聊天流,而不是等待 PFC 实例结束
+# TODO: 优化 idle 逻辑 增强其与 PFC 模式的联动
from typing import Optional, Dict, Set
import asyncio
import time
diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py
index 0a4352e1..f5f11140 100644
--- a/src/plugins/PFC/chat_observer.py
+++ b/src/plugins/PFC/chat_observer.py
@@ -11,7 +11,7 @@ from rich.traceback import install
install(extra_lines=3)
-logger = get_module_logger("chat_observer")
+logger = get_module_logger("pfc_chat_observer")
class ChatObserver:
diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py
index 6132ae13..cd96f4ae 100644
--- a/src/plugins/PFC/conversation.py
+++ b/src/plugins/PFC/conversation.py
@@ -10,7 +10,7 @@ from ..chat.message import Message # 假设 Message 类型被 _convert_to_messa
from src.config.config import global_config
from ..person_info.person_info import person_info_manager
from ..person_info.relationship_manager import relationship_manager
-from ..moods.moods import MoodManager
+from src.manager.mood_manager import mood_manager
from .pfc_relationship import PfcRelationshipUpdater, PfcRepationshipTranslator
from .pfc_emotion import PfcEmotionUpdater
@@ -57,7 +57,7 @@ class Conversation:
self.person_info_mng = person_info_manager
self.relationship_mng = relationship_manager
- self.mood_mng = MoodManager.get_instance()
+ self.mood_mng = mood_manager
self.relationship_updater: Optional[PfcRelationshipUpdater] = None
self.relationship_translator: Optional[PfcRepationshipTranslator] = None
@@ -138,9 +138,6 @@ class Conversation:
logger.debug(f"[私聊][{self.private_name}] 已减少IdleChat活跃实例计数")
if self.observation_info and self.chat_observer: # 确保二者都存在
self.observation_info.unbind_from_chat_observer() # 解绑
- if self.mood_mng and hasattr(self.mood_mng, "stop_mood_update") and self.mood_mng._running: # type: ignore
- self.mood_mng.stop_mood_update() # type: ignore
- logger.debug(f"[私聊][{self.private_name}] MoodManager 后台更新已停止。")
self._initialized = False # 标记为未初始化
logger.info(f"[私聊][{self.private_name}] 对话实例 {self.stream_id} 已停止。")
diff --git a/src/plugins/PFC/conversation_initializer.py b/src/plugins/PFC/conversation_initializer.py
index 118bc171..4e77e49e 100644
--- a/src/plugins/PFC/conversation_initializer.py
+++ b/src/plugins/PFC/conversation_initializer.py
@@ -249,22 +249,6 @@ async def initialize_core_components(conversation_instance: "Conversation"):
# 不需要再次启动,只需确保已初始化
logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) IdleChat实例已初始化")
- if (
- conversation_instance.mood_mng
- and hasattr(conversation_instance.mood_mng, "start_mood_update")
- and not conversation_instance.mood_mng._running
- ): # type: ignore
- conversation_instance.mood_mng.start_mood_update(update_interval=global_config.mood_update_interval) # type: ignore
- logger.debug(
- f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已启动后台更新,间隔: {global_config.mood_update_interval} 秒。"
- )
- elif conversation_instance.mood_mng and conversation_instance.mood_mng._running: # type: ignore
- logger.debug(f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 已在运行中。")
- else:
- logger.warning(
- f"[私聊][{conversation_instance.private_name}] (Initializer) MoodManager 未能启动,相关功能可能受限。"
- )
-
if (
conversation_instance.conversation_info
and conversation_instance.conversation_info.person_id
@@ -299,7 +283,7 @@ async def initialize_core_components(conversation_instance: "Conversation"):
if conversation_instance.conversation_info and conversation_instance.mood_mng: # 确保都存在
try:
conversation_instance.conversation_info.current_emotion_text = (
- conversation_instance.mood_mng.get_prompt()
+ conversation_instance.mood_mng.get_mood_prompt()
) # type: ignore
logger.debug(
f"[私聊][{conversation_instance.private_name}] (Initializer) 初始化时加载情绪文本: {conversation_instance.conversation_info.current_emotion_text}"
diff --git a/src/plugins/PFC/conversation_loop.py b/src/plugins/PFC/conversation_loop.py
index aab031cc..38f2e488 100644
--- a/src/plugins/PFC/conversation_loop.py
+++ b/src/plugins/PFC/conversation_loop.py
@@ -118,7 +118,7 @@ async def run_conversation_loop(conversation_instance: "Conversation"):
conversation_instance.conversation_info.relationship_text = "你们的关系是:普通。"
if conversation_instance.mood_mng:
conversation_instance.conversation_info.current_emotion_text = (
- conversation_instance.mood_mng.get_prompt()
+ conversation_instance.mood_mng.get_mood_prompt()
)
if not all(
diff --git a/src/plugins/PFC/message_sender.py b/src/plugins/PFC/message_sender.py
index 113cffa8..9d9c2c18 100644
--- a/src/plugins/PFC/message_sender.py
+++ b/src/plugins/PFC/message_sender.py
@@ -12,7 +12,7 @@ from rich.traceback import install
install(extra_lines=3)
-logger = get_module_logger("message_sender")
+logger = get_module_logger("pfc_sender")
class DirectMessageSender:
diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py
index a372b3dc..16014e37 100644
--- a/src/plugins/PFC/observation_info.py
+++ b/src/plugins/PFC/observation_info.py
@@ -11,7 +11,7 @@ from src.config.config import global_config
from .chat_observer import ChatObserver
from .chat_states import NotificationHandler, NotificationType, Notification
-logger = get_module_logger("observation_info")
+logger = get_module_logger("pfc_observation_info")
TIME_ZONE = tz.gettz(global_config.TIME_ZONE if global_config else "Asia/Shanghai") # 使用配置的时区,提供默认值
diff --git a/src/plugins/PFC/pfc_emotion.py b/src/plugins/PFC/pfc_emotion.py
index c202d04a..4800a61b 100644
--- a/src/plugins/PFC/pfc_emotion.py
+++ b/src/plugins/PFC/pfc_emotion.py
@@ -3,7 +3,7 @@ from typing import List, Dict, Any
from src.plugins.PFC.chat_observer import ChatObserver
from src.common.logger_manager import get_logger
from src.plugins.models.utils_model import LLMRequest
-from src.plugins.moods.moods import MoodManager # MoodManager 本身是单例
+from src.manager.mood_manager import mood_manager
from src.plugins.utils.chat_message_builder import build_readable_messages
from src.plugins.PFC.observation_info import ObservationInfo
from src.plugins.PFC.conversation_info import ConversationInfo
@@ -19,7 +19,7 @@ class PfcEmotionUpdater:
"""
self.private_name = private_name
self.bot_name = bot_name
- self.mood_mng = MoodManager.get_instance() # 获取 MoodManager 单例
+ self.mood_mng = mood_manager
# LLM 实例 (根据 global_config.llm_summary 配置)
llm_config_summary = getattr(global_config, "llm_summary", None)
@@ -51,7 +51,7 @@ class PfcEmotionUpdater:
logger.error(f"[私聊][{self.private_name}] LLM未初始化,无法进行情绪更新。")
# 即使LLM失败,也应该更新conversation_info中的情绪文本为MoodManager的当前状态
if conversation_info and self.mood_mng:
- conversation_info.current_emotion_text = self.mood_mng.get_prompt()
+ conversation_info.current_emotion_text = self.mood_mng.get_mood_prompt()
return
if not self.mood_mng or not conversation_info or not observation_info:
@@ -97,7 +97,7 @@ class PfcEmotionUpdater:
if (
detected_emotion_word
and detected_emotion_word != "无变化"
- and detected_emotion_word in self.mood_mng.emotion_map
+ and detected_emotion_word in self.mood_mng.EMOTION_FACTOR_MAP
):
self.mood_mng.update_mood_from_emotion(detected_emotion_word, intensity=self.EMOTION_UPDATE_INTENSITY)
logger.debug(
@@ -114,4 +114,4 @@ class PfcEmotionUpdater:
# 无论LLM判断如何,都更新conversation_info中的情绪文本以供Prompt使用
if conversation_info and self.mood_mng: # 确保conversation_info有效
- conversation_info.current_emotion_text = self.mood_mng.get_prompt()
+ conversation_info.current_emotion_text = self.mood_mng.get_mood_prompt()
diff --git a/src/plugins/PFC/pfc_processor.py b/src/plugins/PFC/pfc_processor.py
index ea9ac4df..27597919 100644
--- a/src/plugins/PFC/pfc_processor.py
+++ b/src/plugins/PFC/pfc_processor.py
@@ -1,3 +1,4 @@
+# TODO: 人格侧写(不要把人格侧写的功能实现写到这里!新建文件去)
import traceback
import re
from typing import Any
diff --git a/src/plugins/PFC/pfc_utils.py b/src/plugins/PFC/pfc_utils.py
index 3747dc13..0acf7158 100644
--- a/src/plugins/PFC/pfc_utils.py
+++ b/src/plugins/PFC/pfc_utils.py
@@ -669,7 +669,7 @@ async def build_chat_history_text(observation_info: ObservationInfo, private_nam
if hasattr(observation_info, "chat_history_str") and observation_info.chat_history_str:
chat_history_text = observation_info.chat_history_str
elif hasattr(observation_info, "chat_history") and observation_info.chat_history:
- history_slice = observation_info.chat_history[-20:]
+ history_slice = observation_info.chat_history[-global_config.pfc_recent_history_display_count :]
chat_history_text = await build_readable_messages(
history_slice, replace_bot_name=True, merge_messages=False, timestamp_mode="relative", read_mark=0.0
)
diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py
index adfa33a8..d65a1b24 100644
--- a/src/plugins/PFC/reply_checker.py
+++ b/src/plugins/PFC/reply_checker.py
@@ -4,7 +4,7 @@ from src.config.config import global_config # 为了获取 BOT_QQ
from .chat_observer import ChatObserver
import re
-logger = get_module_logger("reply_checker")
+logger = get_module_logger("pfc_checker")
class ReplyChecker:
diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py
index 6225859b..02b97078 100644
--- a/src/plugins/PFC/reply_generator.py
+++ b/src/plugins/PFC/reply_generator.py
@@ -12,7 +12,7 @@ from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo
from .pfc_utils import build_chat_history_text
-logger = get_logger("reply_generator")
+logger = get_logger("pfc_reply")
PROMPT_GER_VARIATIONS = [
("不用输出或提及对方的网名或绰号", 0.50),
diff --git a/src/plugins/PFC/waiter.py b/src/plugins/PFC/waiter.py
index 9ffff524..a30a9df3 100644
--- a/src/plugins/PFC/waiter.py
+++ b/src/plugins/PFC/waiter.py
@@ -5,7 +5,7 @@ from ...config.config import global_config
import time
import asyncio
-logger = get_module_logger("waiter")
+logger = get_module_logger("pfc_waiter")
# --- 在这里设定你想要的超时时间(秒) ---
# 例如: 120 秒 = 2 分钟
diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py
index 2e057e6f..631d9bbb 100644
--- a/src/plugins/__init__.py
+++ b/src/plugins/__init__.py
@@ -6,7 +6,6 @@ MaiMBot插件系统
from .chat.chat_stream import chat_manager
from .emoji_system.emoji_manager import emoji_manager
from .person_info.relationship_manager import relationship_manager
-from .moods.moods import MoodManager
from .willing.willing_manager import willing_manager
from .schedule.schedule_generator import bot_schedule
@@ -15,7 +14,6 @@ __all__ = [
"chat_manager",
"emoji_manager",
"relationship_manager",
- "MoodManager",
"willing_manager",
"bot_schedule",
]
diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py
index ed22ef2b..23412ecd 100644
--- a/src/plugins/chat/bot.py
+++ b/src/plugins/chat/bot.py
@@ -1,14 +1,13 @@
+import traceback
from typing import Dict, Any
-from ..moods.moods import MoodManager # 导入情绪管理器
-from ...config.config import global_config
-from .message import MessageRecv
-
from src.common.logger_manager import get_logger
+from src.manager.mood_manager import mood_manager # 导入情绪管理器
+from .message import MessageRecv
from ..heartFC_chat.heartflow_processor import HeartFCProcessor
from ..PFC.pfc_processor import PFCProcessor
from ..utils.prompt_builder import Prompt, global_prompt_manager
-import traceback
+from ...config.config import global_config
# 定义日志配置
@@ -21,7 +20,7 @@ class ChatBot:
def __init__(self):
self.bot = None # bot 实例引用
self._started = False
- self.mood_manager = MoodManager.get_instance() # 获取情绪管理器单例
+ self.mood_manager = mood_manager # 获取情绪管理器单例
self.heartflow_processor = HeartFCProcessor() # 新增
self.pfc_processor = PFCProcessor()
@@ -67,12 +66,14 @@ class ChatBot:
logger.debug(f"用户{userinfo.user_id}被禁止回复")
return
- if groupinfo is None:
+ if groupinfo is None and global_config.enable_friend_whitelist:
logger.trace("检测到私聊消息,检查")
# 好友黑名单拦截
if userinfo.user_id not in global_config.talk_allowed_private:
logger.debug(f"用户{userinfo.user_id}没有私聊权限")
return
+ elif not global_config.enable_friend_whitelist:
+ logger.debug("私聊白名单模式未启用,跳过私聊权限检查。")
# 群聊黑名单拦截
if groupinfo is not None and groupinfo.group_id not in global_config.talk_allowed_groups:
diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py
index b9c15288..f3747e15 100644
--- a/src/plugins/chat/message.py
+++ b/src/plugins/chat/message.py
@@ -1,3 +1,4 @@
+# TODO: 原生多模态支持
import time
from abc import abstractmethod
from dataclasses import dataclass
diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py
index 53e8f6f6..148a2f69 100644
--- a/src/plugins/chat/utils.py
+++ b/src/plugins/chat/utils.py
@@ -1,21 +1,20 @@
import random
-import time
import re
+import time
from collections import Counter
import jieba
import numpy as np
-from src.common.logger import get_module_logger
+from maim_message import UserInfo
from pymongo.errors import PyMongoError
+from src.common.logger import get_module_logger
+from src.manager.mood_manager import mood_manager
+from .message import MessageRecv
from ..models.utils_model import LLMRequest
from ..utils.typo_generator import ChineseTypoGenerator
-from ...config.config import global_config
-from .message import MessageRecv
-from maim_message import UserInfo
-from ..moods.moods import MoodManager
from ...common.database import db
-
+from ...config.config import global_config
logger = get_module_logger("chat_utils")
@@ -252,7 +251,7 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
if len_text < 12:
split_strength = 0.2
elif len_text < 32:
- split_strength = 0.6
+ split_strength = 0.5
else:
split_strength = 0.7
# 合并概率与分割强度相反
@@ -371,7 +370,7 @@ def process_llm_response(text: str) -> list[str]:
else:
sentences.append(sentence)
- if len(sentences) > max_sentence_num:
+ if len(sentences) > (max_sentence_num * 2):
logger.warning(f"分割后消息数量过多 ({len(sentences)} 条),返回默认回复")
return [f"{global_config.BOT_NICKNAME}不知道哦"]
@@ -405,7 +404,6 @@ def calculate_typing_time(
- 在所有输入结束后,额外加上回车时间0.3秒
- 如果is_emoji为True,将使用固定1秒的输入时间
"""
- mood_manager = MoodManager.get_instance()
# 将0-1的唤醒度映射到-1到1
mood_arousal = mood_manager.current_mood.arousal
# 映射到0.5到2倍的速度系数
@@ -419,8 +417,8 @@ def calculate_typing_time(
if chinese_chars == 1 and len(input_string.strip()) == 1:
return chinese_time * 3 + 0.3 # 加上回车时间
+ total_time = 0
# 正常计算所有字符的输入时间
- total_time = 0.0
for char in input_string:
if "\u4e00" <= char <= "\u9fff": # 判断是否为中文字符
total_time += chinese_time
@@ -617,7 +615,7 @@ def translate_timestamp_to_human_readable(timestamp: float, mode: str = "normal"
str: 格式化后的时间字符串
"""
if mode == "normal":
- return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp))
+ return time.strftime("%Y-%m-%d %H:%M:%S ", time.localtime(timestamp))
elif mode == "relative":
now = time.time()
diff = now - timestamp
diff --git a/src/plugins/group_nickname/nickname_manager.py b/src/plugins/group_nickname/nickname_manager.py
index 836ba2a3..68210572 100644
--- a/src/plugins/group_nickname/nickname_manager.py
+++ b/src/plugins/group_nickname/nickname_manager.py
@@ -144,7 +144,7 @@ class NicknameManager:
self.nickname_queue: asyncio.Queue = asyncio.Queue(maxsize=self.queue_max_size)
self._stop_event = threading.Event() # stop_event 仍然使用 threading.Event,因为它是由另一个线程设置的
self._nickname_thread: Optional[threading.Thread] = None
- self.sleep_interval = getattr(self.config, "nickname_process_sleep_interval", 60) # 超时时间
+ self.sleep_interval = getattr(self.config, "nickname_process_sleep_interval", 5) # 超时时间
self._initialized = True
logger.info("NicknameManager 初始化完成。")
@@ -177,6 +177,7 @@ class NicknameManager:
self._stop_event.set() # 设置停止事件,_processing_loop 会检测到
try:
# 不需要清空 asyncio.Queue,让循环自然结束或被取消
+ # self.empty_queue(self.nickname_queue)
self._nickname_thread.join(timeout=10) # 等待线程结束
if self._nickname_thread.is_alive():
logger.warning("绰号处理器线程在超时后仍未停止。")
@@ -189,6 +190,13 @@ class NicknameManager:
else:
logger.info("绰号处理器线程未在运行或已被清理。")
+ # def empty_queue(self, q: asyncio.Queue):
+ # while not q.empty():
+ # # Depending on your program, you may want to
+ # # catch QueueEmpty
+ # q.get_nowait()
+ # q.task_done()
+
async def trigger_nickname_analysis(
self,
anchor_message: MessageRecv,
@@ -202,7 +210,7 @@ class NicknameManager:
if not self.is_enabled:
return
- if random.random() > 0.9:
+ if random.random() < global_config.nickname_analysis_probability:
logger.debug("跳过绰号分析:随机概率未命中。")
return
@@ -358,7 +366,6 @@ class NicknameManager:
logger.info(f"{log_prefix} LLM 找到绰号映射,准备更新数据库: {nickname_map_to_update}")
for user_id_str, nickname in nickname_map_to_update.items():
- # ... (验证和数据库更新逻辑保持不变) ...
if not user_id_str or not nickname:
logger.warning(f"{log_prefix} 跳过无效条目: user_id='{user_id_str}', nickname='{nickname}'")
continue
@@ -394,7 +401,6 @@ class NicknameManager:
"""
内部方法:调用 LLM 分析聊天记录和 Bot 回复,提取可靠的 用户ID-绰号 映射。
"""
- # ... (此方法内部逻辑保持不变) ...
if not self.llm_mapper:
logger.error("LLM 映射器未初始化,无法执行分析。")
return {"is_exist": False}
diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py
index e96e1775..cd951130 100644
--- a/src/plugins/heartFC_chat/heartFC_chat.py
+++ b/src/plugins/heartFC_chat/heartFC_chat.py
@@ -1,34 +1,36 @@
import asyncio
+import contextlib
+import json # <--- 确保导入 json
+import random # <--- 添加导入
import time
import re
import traceback
-import random
-import 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
-from src.plugins.chat.message import Seg
-from src.plugins.chat.chat_stream import ChatStream
-from src.plugins.chat.message import UserInfo
-from src.plugins.chat.chat_stream import chat_manager
-from src.common.logger_manager import get_logger
-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
-from src.plugins.utils.timer_calculator import Timer
-from src.plugins.emoji_system.emoji_manager import emoji_manager
-from src.heart_flow.sub_mind import SubMind
-from src.heart_flow.observation import Observation
-from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
-import contextlib
-from src.plugins.utils.chat_message_builder import num_new_messages_since
-from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
-from .heartFC_sender import HeartFCSender
-from src.plugins.chat.utils import process_llm_response
-from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
-from src.plugins.moods.moods import MoodManager
-from src.heart_flow.utils_chat import get_chat_type_and_target_info
+from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
+
from rich.traceback import install
+
+from src.common.logger_manager import get_logger
+from src.config.config import global_config
+from src.heart_flow.observation import Observation
+from src.heart_flow.sub_mind import SubMind
+from src.heart_flow.utils_chat import get_chat_type_and_target_info
+from src.manager.mood_manager import mood_manager
+from src.plugins.chat.chat_stream import ChatStream
+from src.plugins.chat.chat_stream import chat_manager
+from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
+from src.plugins.chat.message import Seg # Local import needed after move
+from src.plugins.chat.message import UserInfo
+from src.plugins.chat.utils import process_llm_response
+from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
+from src.plugins.emoji_system.emoji_manager import emoji_manager
+from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
+from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
+from src.plugins.models.utils_model import LLMRequest
+from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
+from src.plugins.utils.chat_message_builder import num_new_messages_since
+from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
+from .heartFC_sender import HeartFCSender
from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat
from src.plugins.group_nickname.nickname_manager import nickname_manager
@@ -808,37 +810,37 @@ class HeartFChatting:
actions_to_remove_temporarily = []
# --- 检查历史动作并决定临时移除动作 (逻辑保持不变) ---
- 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
- 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}")
+ # 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
+ # 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:
- logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (触发)")
- actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
- else:
- logger.info(
- f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (未触发)"
- )
- elif lian_xu_wen_ben_hui_fu == 1:
- 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 (未触发)")
+ # 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:
+ # logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (触发)")
+ # actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
+ # else:
+ # logger.info(
+ # f"{self.log_prefix}[Planner] 连续回复 2 次,80% 概率移除 text_reply 和 emoji_reply (未触发)"
+ # )
+ # elif lian_xu_wen_ben_hui_fu == 1:
+ # 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 (未触发)")
# --- 结束检查历史动作 ---
# 获取观察信息
@@ -1312,7 +1314,7 @@ class HeartFChatting:
"""
try:
# 1. 获取情绪影响因子并调整模型温度
- arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()
+ arousal_multiplier = mood_manager.get_arousal_multiplier()
current_temp = global_config.llm_normal["temp"] * arousal_multiplier
self.model_normal.temperature = current_temp # 动态调整温度
diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py
index 4daa4d3f..abc3d14c 100644
--- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py
+++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py
@@ -10,7 +10,7 @@ from src.plugins.person_info.relationship_manager import relationship_manager
from src.plugins.chat.utils import get_embedding
from ...common.database import db
from ..chat.utils import get_recent_group_speaker
-from ..moods.moods import MoodManager
+from src.manager.mood_manager import mood_manager
from ..memory_system.Hippocampus import HippocampusManager
from ..schedule.schedule_generator import bot_schedule
from ..knowledge.knowledge_lib import qa_manager
@@ -29,15 +29,15 @@ def init_prompt():
{chat_target}
{chat_talking_prompt}
现在你想要回复或参与讨论。\n
-你是{bot_name},{prompt_personality}。
-你正在{chat_target_2},现在请你读读之前的聊天记录,可以自然随意一些,简短一些,就像群聊里的真人一样,注意把握聊天内容,整体风格可以平和、简短。
+你是{bot_name}。你正在{chat_target_2}
+
看到以上聊天记录,你刚刚在想:
-
{current_mind_info}
-因为上述想法,你决定发言,原因是:{reason}
+因为上述想法,你决定发言。
-回复尽量简短一些。请注意把握聊天内容,{reply_style2}。请一次只回复一个话题,不要同时回复多个人。{prompt_ger}
-{reply_style1},说中文,不要刻意突出自身学科背景,注意只输出回复内容,不要去主动讨论或评价别人发的表情包,它们只是一种辅助表达方式。
+现在请你读读之前的聊天记录,把你的想法组织成合适简短的语言,然后发一条消息,可以自然随意一些,简短一些,就像群聊里的真人一样,注意把握聊天内容,整体风格可以平和、简短,避免超出你内心想法的范围
+这条消息可以尽量简短一些。{reply_style2}。请一次只回复一个话题,不要同时回复多个人。{prompt_ger}
+{reply_style1},说中文,不要刻意突出自身学科背景,注意只输出消息内容,不要去主动讨论或评价别人发的表情包,它们只是一种辅助表达方式。
{moderation_prompt}。注意:回复不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""",
"heart_flow_prompt",
)
@@ -46,56 +46,97 @@ def init_prompt():
"""
你有以下信息可供参考:
{structured_info}
-以上的消息是你获取到的消息,或许可以帮助你更好地回复。
+以上的信息是你获取到的消息,或许可以帮助你更好地回复。
""",
"info_from_tools",
)
# Planner提示词 - 修改为要求 JSON 输出
Prompt(
- """你的名字是{bot_name},{prompt_personality},{chat_context_description}。需要基于以下信息决定如何参与对话:
-{structured_info_block}
-{nickname_info}
-{chat_content_block}
-{current_mind_block}
-{cycle_info_block}
+ """
+
+现在{bot_name}开始在一个qq群聊中专注聊天。你需要操控{bot_name},并且根据以下信息决定是否,如何参与对话。
+
-请综合分析聊天内容和你看到的新消息,参考内心想法,并根据以下原则和可用动作做出决策。
+
+
+ {bot_name}
+ {nickname_info}
+
-【发送新消息原则】
-1. 不发送新消息(no_reply)适用:
-- 话题无关/无聊/不感兴趣
-- 最后一条消息是你自己发的且无人回应你
-- 讨论你不懂的专业话题
-- 你发送了太多消息,且无人回复
+
+ {chat_content_block}
+
+
+ {current_mind_block}
+ {cycle_info_block}
+
+
-2. 发送文字消息(text_reply)适用:
-- 有实质性内容需要表达
-- 有人提到你,但你还没有回应他
-- 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合,也就是当前场合)
-- 不要追加太多表情
+
+
+ 请综合分析聊天内容和你看到的新消息,参考{bot_name}的内心想法,并根据以下原则和可用动作灵活谨慎的做出决策,需要符合正常的群聊社交节奏。
+
-3. 发送纯表情(emoji_reply)适用:
-- 适合用表情回应的场景
-- 需提供明确的emoji_query
+
+
+ 1. 以下情况可以不发送新消息(no_reply):
+ - {bot_name}的内心想法表达不想发言
+ - 话题似乎对{bot_name}来说无关/无聊/不感兴趣
+ - 现在说话不太合适了
+ - 最后一条消息是{bot_name}自己发的且无人回应{bot_name},同时{bot_name}也没有别的想要回复的消息
+ - 讨论不了解的专业话题,或你不知道的梗,且对{bot_name}来说似乎没那么重要
+ - {bot_name}发送了太多消息,且无人回复
+ - (特殊情况){bot_name}的内心想法返回错误/无返回
+
-4. 自我对话处理:
-- 如果最后一条消息是你自己发的,而你还想继续发消息,需自然衔接,不要有不自然的内容重叠
-- 避免重复或评价自己的发言
-- 不要自己和自己聊天
+
+ 2. 以下情况可以发送文字消息(text_reply):
+ - 确认内心想法显示{bot_name}想要发言,且有实质内容想表达
+ - 同时确认现在适合发言
+ - 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合,也就是当前场合)
+ - 不要追加太多表情
+
-决策任务
-{action_options_text}
+
+ 3. 发送纯表情(emoji_reply)适用:
+ - {bot_name}似乎想加入话题或继续讨论,但是似乎又没什么实质表达内容
+ - 适合用表情回应的场景
+ - 需提供明确的emoji_query
+ - 群聊里除了{bot_name}以外的大家都在发表情包
+
-你必须从上面列出的可用行动中选择一个,并说明原因。
-你的决策必须以严格的 JSON 格式输出,且仅包含 JSON 内容,不要有任何其他文字或解释。
-JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
-{{
- "action": "string", // 必须是上面提供的可用行动之一 (例如: '{example_action}')
- "reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则
- "emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。
-}}
-请输出你的决策 JSON:
+
+ 4. 对话处理:
+ - 如果最后一条消息是{bot_name}发的,而你还想操控{bot_name}继续发消息,请确保这是合适的(例如{bot_name}确实有合适的补充,或回应之前没回应的消息)
+ - 注意话题的推进,如果没有必要,不要揪着一个话题不放。
+ - 不要让{bot_name}自己和自己聊天
+
+
+
+
+ 决策任务
+ {action_options_text}
+
+
+
+
+
+ 你必须从available_actions列出的可用行动中选择一个,并说明原因。
+ 你的决策必须以严格的 JSON 格式输出,且仅包含 JSON 内容,不要有任何其他文字或解释。
+ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
+
+
+ {{
+ "action": "string", // 必须是上面提供的可用行动之一 (例如: '{example_action}')
+ "reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了decision_principles。
+ "emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。
+ }}
+
+
+ 请输出你的决策 JSON:
+
+
""",
"planner_prompt",
)
@@ -221,18 +262,18 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s
reply_styles1 = [
("给出日常且口语化的回复,平淡一些", 0.4),
("给出非常简短的回复", 0.4),
- ("给出缺失主语的回复,简短", 0.15),
- ("给出带有语病的回复,朴实平淡", 0.05),
+ ("**给出省略主语的回复,简短**", 0.15),
+ ("给出带有语病的回复,朴实平淡", 0.00),
]
reply_style1_chosen = random.choices(
[style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1
)[0]
reply_styles2 = [
- ("不要回复的太有条理,可以有个性", 0.6),
- ("不要回复的太有条理,可以复读", 0.15),
+ ("不要回复的太有条理,可以有个性", 0.8),
+ ("不要回复的太有条理,可以复读", 0.0),
("回复的认真一些", 0.2),
- ("可以回复单个表情符号", 0.05),
+ ("可以回复单个表情符号", 0.00),
]
reply_style2_chosen = random.choices(
[style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1
@@ -351,8 +392,7 @@ class PromptBuilder:
else:
logger.warning(f"Invalid person tuple encountered for relationship prompt: {person}")
- mood_manager = MoodManager.get_instance()
- mood_prompt = mood_manager.get_prompt()
+ mood_prompt = mood_manager.get_mood_prompt()
reply_styles1 = [
("给出日常且口语化的回复,平淡一些", 0.30),
("给出非常简短的回复", 0.30),
@@ -362,8 +402,8 @@ class PromptBuilder:
[style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1
)[0]
reply_styles2 = [
- ("不用回复的太有条理,可以有个性", 0.7), # 60%概率
- ("不用回复的太有条理,可以复读", 0.05), # 15%概率
+ ("不用回复的太有条理,可以有个性", 0.75), # 60%概率
+ ("不用回复的太有条理,可以复读", 0.0), # 15%概率
("回复的认真一些", 0.2), # 20%概率
("可以回复单个表情符号", 0.05), # 5%概率
]
diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py
index c0616a27..5119ef56 100644
--- a/src/plugins/heartFC_chat/normal_chat.py
+++ b/src/plugins/heartFC_chat/normal_chat.py
@@ -1,25 +1,26 @@
-import time
import asyncio
-import traceback
import statistics # 导入 statistics 模块
+import time
+import traceback
from random import random
from typing import List, Optional # 导入 Optional
-from ..moods.moods import MoodManager
-from ...config.config import global_config
-from ..emoji_system.emoji_manager import emoji_manager
-from .normal_chat_generator import NormalChatGenerator
-from ..chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet
-from ..chat.message_sender import message_manager
-from ..chat.utils_image import image_path_to_base64
-from ..willing.willing_manager import willing_manager
from maim_message import UserInfo, Seg
+
from src.common.logger_manager import get_logger
+from src.heart_flow.utils_chat import get_chat_type_and_target_info
+from src.manager.mood_manager import mood_manager
from src.plugins.chat.chat_stream import ChatStream, chat_manager
from src.plugins.person_info.relationship_manager import relationship_manager
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from src.plugins.utils.timer_calculator import Timer
-from src.heart_flow.utils_chat import get_chat_type_and_target_info
+from .normal_chat_generator import NormalChatGenerator
+from ..chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet
+from ..chat.message_sender import message_manager
+from ..chat.utils_image import image_path_to_base64
+from ..emoji_system.emoji_manager import emoji_manager
+from ..willing.willing_manager import willing_manager
+from ...config.config import global_config
from src.plugins.group_nickname.nickname_manager import nickname_manager
@@ -46,7 +47,7 @@ class NormalChat:
# Other sync initializations
self.gpt = NormalChatGenerator()
- self.mood_manager = MoodManager.get_instance()
+ self.mood_manager = mood_manager
self.start_time = time.time()
self.last_speak_time = 0
self._chat_task: Optional[asyncio.Task] = None
diff --git a/src/plugins/knowledge/src/qa_manager.py b/src/plugins/knowledge/src/qa_manager.py
index 11067d0e..8f9266d6 100644
--- a/src/plugins/knowledge/src/qa_manager.py
+++ b/src/plugins/knowledge/src/qa_manager.py
@@ -61,7 +61,7 @@ class QAManager:
for res in relation_search_res:
rel_str = self.embed_manager.relation_embedding_store.store.get(res[0]).str
- print(f"找到相关关系,相似度:{(res[1] * 100):.2f}% - {rel_str}")
+ logger.debug(f"找到相关关系,相似度:{(res[1] * 100):.2f}% - {rel_str}")
# TODO: 使用LLM过滤三元组结果
# logger.info(f"LLM过滤三元组用时:{time.time() - part_start_time:.2f}s")
@@ -77,16 +77,16 @@ class QAManager:
logger.debug(f"文段检索用时:{part_end_time - part_start_time:.5f}s")
if len(relation_search_res) != 0:
- logger.info("找到相关关系,将使用RAG进行检索")
+ logger.debug("找到相关关系,将使用RAG进行检索")
# 使用KG检索
part_start_time = time.perf_counter()
result, ppr_node_weights = self.kg_manager.kg_search(
relation_search_res, paragraph_search_res, self.embed_manager
)
part_end_time = time.perf_counter()
- logger.info(f"RAG检索用时:{part_end_time - part_start_time:.5f}s")
+ logger.debug(f"RAG检索用时:{part_end_time - part_start_time:.5f}s")
else:
- logger.info("未找到相关关系,将使用文段检索结果")
+ logger.debug("未找到相关关系,将使用文段检索结果")
result = paragraph_search_res
ppr_node_weights = None
@@ -95,7 +95,7 @@ class QAManager:
for res in result:
raw_paragraph = self.embed_manager.paragraphs_embedding_store.store[res[0]].str
- print(f"找到相关文段,相关系数:{res[1]:.8f}\n{raw_paragraph}\n\n")
+ logger.debug(f"找到相关文段,相关系数:{res[1]:.8f}\n{raw_paragraph}\n\n")
return result, ppr_node_weights
else:
diff --git a/src/plugins/moods/moods.py b/src/plugins/moods/moods.py
deleted file mode 100644
index 1c025319..00000000
--- a/src/plugins/moods/moods.py
+++ /dev/null
@@ -1,293 +0,0 @@
-import math
-import threading
-import time
-from dataclasses import dataclass
-
-from ...config.config import global_config
-from src.common.logger_manager import get_logger
-from ..person_info.relationship_manager import relationship_manager
-from src.individuality.individuality import Individuality
-
-
-logger = get_logger("mood")
-
-
-@dataclass
-class MoodState:
- valence: float # 愉悦度 (-1.0 到 1.0),-1表示极度负面,1表示极度正面
- arousal: float # 唤醒度 (-1.0 到 1.0),-1表示抑制,1表示兴奋
- text: str # 心情文本描述
-
-
-class MoodManager:
- _instance = None
- _lock = threading.Lock()
-
- def __new__(cls):
- with cls._lock:
- if cls._instance is None:
- cls._instance = super().__new__(cls)
- cls._instance._initialized = False
- return cls._instance
-
- def __init__(self):
- # 确保初始化代码只运行一次
- if self._initialized:
- return
-
- self._initialized = True
-
- # 初始化心情状态
- self.current_mood = MoodState(valence=0.0, arousal=0.0, text="平静")
-
- # 从配置文件获取衰减率
- self.decay_rate_valence = 1 - global_config.mood_decay_rate # 愉悦度衰减率
- self.decay_rate_arousal = 1 - global_config.mood_decay_rate # 唤醒度衰减率
-
- # 上次更新时间
- self.last_update = time.time()
-
- # 线程控制
- self._running = False
- self._update_thread = None
-
- # 情绪词映射表 (valence, arousal)
- self.emotion_map = {
- "开心": (0.21, 0.6),
- "害羞": (0.15, 0.2),
- "愤怒": (-0.24, 0.8),
- "恐惧": (-0.21, 0.7),
- "悲伤": (-0.21, 0.3),
- "厌恶": (-0.12, 0.4),
- "惊讶": (0.06, 0.7),
- "困惑": (0.0, 0.6),
- "平静": (0.03, 0.5),
- }
-
- # 情绪文本映射表
- self.mood_text_map = {
- # 第一象限:高唤醒,正愉悦
- (0.5, 0.4): "兴奋",
- (0.3, 0.6): "快乐",
- (0.2, 0.3): "满足",
- # 第二象限:高唤醒,负愉悦
- (-0.5, 0.4): "愤怒",
- (-0.3, 0.6): "焦虑",
- (-0.2, 0.3): "烦躁",
- # 第三象限:低唤醒,负愉悦
- (-0.5, -0.4): "悲伤",
- (-0.3, -0.3): "疲倦",
- (-0.4, -0.7): "疲倦",
- # 第四象限:低唤醒,正愉悦
- (0.2, -0.1): "平静",
- (0.3, -0.2): "安宁",
- (0.5, -0.4): "放松",
- }
-
- @classmethod
- def get_instance(cls) -> "MoodManager":
- """获取MoodManager的单例实例"""
- if cls._instance is None:
- cls._instance = MoodManager()
- return cls._instance
-
- def start_mood_update(self, update_interval: float = 5.0) -> None:
- """
- 启动情绪更新线程
- :param update_interval: 更新间隔(秒)
- """
- if self._running:
- return
-
- self._running = True
- self._update_thread = threading.Thread(
- target=self._continuous_mood_update, args=(update_interval,), daemon=True
- )
- self._update_thread.start()
-
- def stop_mood_update(self) -> None:
- """停止情绪更新线程"""
- self._running = False
- if self._update_thread and self._update_thread.is_alive():
- self._update_thread.join()
-
- def _continuous_mood_update(self, update_interval: float) -> None:
- """
- 持续更新情绪状态的线程函数
- :param update_interval: 更新间隔(秒)
- """
- while self._running:
- self._apply_decay()
- self._update_mood_text()
- time.sleep(update_interval)
-
- def _apply_decay(self) -> None:
- """应用情绪衰减,正向和负向情绪分开计算"""
- current_time = time.time()
- time_diff = current_time - self.last_update
- agreeableness_factor = 1
- agreeableness_bias = 0
- neuroticism_factor = 0.5
-
- # 获取人格特质
- personality = Individuality.get_instance().personality
- if personality:
- # 神经质:影响情绪变化速度
- neuroticism_factor = 1 + (personality.neuroticism - 0.5) * 0.4
- agreeableness_factor = 1 + (personality.agreeableness - 0.5) * 0.4
-
- # 宜人性:影响情绪基准线
- if personality.agreeableness < 0.2:
- agreeableness_bias = (personality.agreeableness - 0.2) * 0.5
- elif personality.agreeableness > 0.8:
- agreeableness_bias = (personality.agreeableness - 0.8) * 0.5
- else:
- agreeableness_bias = 0
-
- # 分别计算正向和负向的衰减率
- if self.current_mood.valence >= 0:
- # 正向情绪衰减
- decay_rate_positive = self.decay_rate_valence * (1 / agreeableness_factor)
- valence_target = 0 + agreeableness_bias
- self.current_mood.valence = valence_target + (self.current_mood.valence - valence_target) * math.exp(
- -decay_rate_positive * time_diff * neuroticism_factor
- )
- else:
- # 负向情绪衰减
- decay_rate_negative = self.decay_rate_valence * agreeableness_factor
- valence_target = 0 + agreeableness_bias
- self.current_mood.valence = valence_target + (self.current_mood.valence - valence_target) * math.exp(
- -decay_rate_negative * time_diff * neuroticism_factor
- )
-
- # Arousal 向中性(0)回归
- arousal_target = 0
- self.current_mood.arousal = arousal_target + (self.current_mood.arousal - arousal_target) * math.exp(
- -self.decay_rate_arousal * time_diff * neuroticism_factor
- )
-
- # 确保值在合理范围内
- self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
- self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
-
- self.last_update = current_time
-
- def update_mood_from_text(self, text: str, valence_change: float, arousal_change: float) -> None:
- """根据输入文本更新情绪状态"""
-
- self.current_mood.valence += valence_change
- self.current_mood.arousal += arousal_change
-
- # 限制范围
- self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
- self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
-
- self._update_mood_text()
-
- def set_mood_text(self, text: str) -> None:
- """直接设置心情文本"""
- self.current_mood.text = text
-
- def _update_mood_text(self) -> None:
- """根据当前情绪状态更新文本描述"""
- closest_mood = None
- min_distance = float("inf")
-
- for (v, a), text in self.mood_text_map.items():
- distance = math.sqrt((self.current_mood.valence - v) ** 2 + (self.current_mood.arousal - a) ** 2)
- if distance < min_distance:
- min_distance = distance
- closest_mood = text
-
- if closest_mood:
- self.current_mood.text = closest_mood
-
- def update_mood_by_user(self, user_id: str, valence_change: float, arousal_change: float) -> None:
- """根据用户ID更新情绪状态"""
-
- # 这里可以根据用户ID添加特定的权重或规则
- weight = 1.0 # 默认权重
-
- self.current_mood.valence += valence_change * weight
- self.current_mood.arousal += arousal_change * weight
-
- # 限制范围
- self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
- self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
-
- self._update_mood_text()
-
- def get_prompt(self) -> str:
- """根据当前情绪状态生成提示词"""
-
- base_prompt = f"当前心情:{self.current_mood.text}。"
-
- # 根据情绪状态添加额外的提示信息
- if self.current_mood.valence > 0.5:
- base_prompt += "你现在心情很好,"
- elif self.current_mood.valence < -0.5:
- base_prompt += "你现在心情不太好,"
-
- if self.current_mood.arousal > 0.4:
- base_prompt += "情绪比较激动。"
- elif self.current_mood.arousal < -0.4:
- base_prompt += "情绪比较平静。"
-
- return base_prompt
-
- def get_arousal_multiplier(self) -> float:
- """根据当前情绪状态返回唤醒度乘数"""
- if self.current_mood.arousal > 0.4:
- multiplier = 1 + min(0.15, (self.current_mood.arousal - 0.4) / 3)
- return multiplier
- elif self.current_mood.arousal < -0.4:
- multiplier = 1 - min(0.15, ((0 - self.current_mood.arousal) - 0.4) / 3)
- return multiplier
- return 1.0
-
- def get_current_mood(self) -> MoodState:
- """获取当前情绪状态"""
- return self.current_mood
-
- def print_mood_status(self) -> None:
- """打印当前情绪状态"""
- logger.info(
- f"愉悦度: {self.current_mood.valence:.2f}, "
- f"唤醒度: {self.current_mood.arousal:.2f}, "
- f"心情: {self.current_mood.text}"
- )
-
- def update_mood_from_emotion(self, emotion: str, intensity: float = 1.0) -> None:
- """
- 根据情绪词更新心情状态
- :param emotion: 情绪词(如'happy', 'sad'等)
- :param intensity: 情绪强度(0.0-1.0)
- """
- if emotion not in self.emotion_map:
- logger.debug(f"[情绪更新] 未知情绪词: {emotion}")
- return
-
- valence_change, arousal_change = self.emotion_map[emotion]
- old_valence = self.current_mood.valence
- old_arousal = self.current_mood.arousal
- old_mood = self.current_mood.text
-
- valence_change = relationship_manager.feedback_to_mood(valence_change)
-
- # 应用情绪强度
- valence_change *= intensity
- arousal_change *= intensity
-
- # 更新当前情绪状态
- self.current_mood.valence += valence_change
- self.current_mood.arousal += arousal_change
-
- # 限制范围
- self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
- self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
-
- self._update_mood_text()
-
- logger.info(
- f"[情绪变化] {emotion}(强度:{intensity:.2f}) | 愉悦度:{old_valence:.2f}->{self.current_mood.valence:.2f}, 唤醒度:{old_arousal:.2f}->{self.current_mood.arousal:.2f} | 心情:{old_mood}->{self.current_mood.text}"
- )
diff --git a/src/plugins/person_info/relationship_manager.py b/src/plugins/person_info/relationship_manager.py
index 730e686b..ce86eb62 100644
--- a/src/plugins/person_info/relationship_manager.py
+++ b/src/plugins/person_info/relationship_manager.py
@@ -8,6 +8,9 @@ import random
from typing import List, Dict
from ...common.database import db
from maim_message import UserInfo
+
+from ...manager.mood_manager import mood_manager
+
# import re
# import traceback
@@ -24,9 +27,7 @@ class RelationshipManager:
@property
def mood_manager(self):
if self._mood_manager is None:
- from ..moods.moods import MoodManager # 延迟导入
-
- self._mood_manager = MoodManager.get_instance()
+ self._mood_manager = mood_manager
return self._mood_manager
def positive_feedback_sys(self, label: str, stance: str):
@@ -62,9 +63,7 @@ class RelationshipManager:
def mood_feedback(self, value):
"""情绪反馈"""
mood_manager = self.mood_manager
- mood_gain = mood_manager.get_current_mood().valence ** 2 * math.copysign(
- 1, value * mood_manager.get_current_mood().valence
- )
+ mood_gain = mood_manager.current_mood.valence**2 * math.copysign(1, value * mood_manager.current_mood.valence)
value += value * mood_gain
logger.info(f"当前relationship增益系数:{mood_gain:.3f}")
return value
diff --git a/src/plugins/remote/__init__.py b/src/plugins/remote/__init__.py
deleted file mode 100644
index 4cbce96d..00000000
--- a/src/plugins/remote/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .remote import main
-
-# 启动心跳线程
-heartbeat_thread = main()
diff --git a/src/plugins/remote/remote.py b/src/plugins/remote/remote.py
index 68b02396..1d26df01 100644
--- a/src/plugins/remote/remote.py
+++ b/src/plugins/remote/remote.py
@@ -1,248 +1,142 @@
+import asyncio
+
import requests
-import time
-import uuid
import platform
-import os
-import json
-import threading
-import subprocess
# from loguru import logger
from src.common.logger_manager import get_logger
from src.config.config import global_config
+from src.manager.async_task_manager import AsyncTask
+from src.manager.local_store_manager import local_storage
logger = get_logger("remote")
-# --- 使用向上导航的方式定义路径 ---
-
-# 1. 获取当前文件 (remote.py) 所在的目录
-current_dir = os.path.dirname(os.path.abspath(__file__))
-
-# 2. 从当前目录向上导航三级找到项目根目录
-# (src/plugins/remote/ -> src/plugins/ -> src/ -> project_root)
-root_dir = os.path.abspath(os.path.join(current_dir, "..", "..", ".."))
-
-# 3. 定义 data 目录的路径 (位于项目根目录下)
-data_dir = os.path.join(root_dir, "data")
-
-# 4. 定义 UUID 文件在 data 目录下的完整路径
-UUID_FILE = os.path.join(data_dir, "client_uuid.json")
-
-# --- 路径定义结束 ---
+TELEMETRY_SERVER_URL = "http://localhost:8080"
+"""遥测服务地址"""
-# 生成或获取客户端唯一ID
-def get_unique_id():
- # --- 在尝试读写 UUID_FILE 之前确保 data 目录存在 ---
- # 将目录检查和创建逻辑移到这里,在首次需要写入前执行
- try:
- # exist_ok=True 意味着如果目录已存在也不会报错
- os.makedirs(data_dir, exist_ok=True)
- except OSError as e:
- # 处理可能的权限错误等
- logger.error(f"无法创建数据目录 {data_dir}: {e}")
- # 根据你的错误处理逻辑,可能需要在这里返回错误或抛出异常
- # 暂且返回 None 或抛出,避免继续执行导致问题
- raise RuntimeError(f"无法创建必要的数据目录 {data_dir}") from e
- # --- 目录检查结束 ---
+class TelemetryHeartBeatTask(AsyncTask):
+ HEARTBEAT_INTERVAL = 300
- # 检查是否已经有保存的UUID
- if os.path.exists(UUID_FILE):
- try:
- with open(UUID_FILE, "r", encoding="utf-8") as f: # 指定 encoding
- data = json.load(f)
- if "client_id" in data:
- logger.debug(f"从本地文件读取客户端ID: {UUID_FILE}")
- return data["client_id"]
- except (json.JSONDecodeError, IOError) as e:
- logger.warning(f"读取UUID文件 {UUID_FILE} 出错: {e},将生成新的UUID")
- except Exception as e: # 捕捉其他可能的异常
- logger.error(f"读取UUID文件 {UUID_FILE} 时发生未知错误: {e}")
+ def __init__(self):
+ super().__init__(task_name="Telemetry Heart Beat Task", run_interval=self.HEARTBEAT_INTERVAL)
+ self.server_url = TELEMETRY_SERVER_URL
+ """遥测服务地址"""
- # 如果没有保存的UUID或读取出错,则生成新的
- client_id = generate_unique_id()
- logger.info(f"生成新的客户端ID: {client_id}")
+ self.client_uuid = local_storage["mmc_uuid"] if "mmc_uuid" in local_storage else None
+ """客户端UUID"""
- # 保存UUID到文件
- try:
- # 再次确认目录存在 (虽然理论上前面已创建,但更保险)
- os.makedirs(data_dir, exist_ok=True)
- with open(UUID_FILE, "w", encoding="utf-8") as f: # 指定 encoding
- json.dump({"client_id": client_id}, f, indent=4) # 添加 indent 使json可读
- logger.info(f"已保存新生成的客户端ID到本地文件: {UUID_FILE}")
- except IOError as e:
- logger.error(f"保存UUID时出错: {UUID_FILE} - {e}")
- except Exception as e: # 捕捉其他可能的异常
- logger.error(f"保存UUID文件 {UUID_FILE} 时发生未知错误: {e}")
+ self.info_dict = self._get_sys_info()
+ """系统信息字典"""
- return client_id
+ @staticmethod
+ def _get_sys_info() -> dict[str, str]:
+ """获取系统信息"""
+ info_dict = {
+ "os_type": "Unknown",
+ "py_version": platform.python_version(),
+ "mmc_version": global_config.MAI_VERSION,
+ }
+ match platform.system():
+ case "Windows":
+ info_dict["os_type"] = "Windows"
+ case "Linux":
+ info_dict["os_type"] = "Linux"
+ case "Darwin":
+ info_dict["os_type"] = "macOS"
+ case _:
+ info_dict["os_type"] = "Unknown"
-# 生成客户端唯一ID
-def generate_unique_id():
- # 基于机器码生成唯一ID,同一台机器上生成的UUID是固定的,只要机器码不变
- import hashlib
+ return info_dict
- system_info = platform.system()
- machine_code = None
+ async def _req_uuid(self) -> bool:
+ """
+ 向服务端请求UUID(不应在已存在UUID的情况下调用,会覆盖原有的UUID)
+ """
- try:
- if system_info == "Windows":
- # 使用wmic命令获取主机UUID(更稳定)
- result = subprocess.check_output(
- "wmic csproduct get uuid", shell=True, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
- )
- lines = result.decode(errors="ignore").splitlines()
- # 过滤掉空行和表头,只取有效UUID
- uuids = [line.strip() for line in lines if line.strip() and line.strip().lower() != "uuid"]
- if uuids:
- uuid_val = uuids[0]
- # logger.debug(f"主机UUID: {uuid_val}")
- # 增加无效值判断
- if uuid_val and uuid_val.lower() not in ["to be filled by o.e.m.", "none", "", "standard"]:
- machine_code = uuid_val
- elif system_info == "Linux":
- # 优先读取 /etc/machine-id,其次 /var/lib/dbus/machine-id,取第一个非空且内容有效的
- for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"]:
- if os.path.exists(path):
- with open(path, "r") as f:
- code = f.read().strip()
- # 只要内容非空且不是全0
- if code and set(code) != {"0"}:
- machine_code = code
- break
- elif system_info == "Darwin":
- # macOS: 使用IOPlatformUUID
- result = subprocess.check_output(
- "ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/'", shell=True
- )
- uuid_line = result.decode(errors="ignore")
- # 解析出 "IOPlatformUUID" = "xxxx-xxxx-xxxx-xxxx"
- import re
-
- m = re.search(r'"IOPlatformUUID"\s*=\s*"([^"]+)"', uuid_line)
- if m:
- uuid_val = m.group(1)
- logger.debug(f"IOPlatformUUID: {uuid_val}")
- if uuid_val and uuid_val.lower() not in ["to be filled by o.e.m.", "none", "", "standard"]:
- machine_code = uuid_val
- except Exception as e:
- logger.debug(f"获取机器码失败: {e}")
-
- # 如果主板序列号无效,尝试用MAC地址
- if not machine_code:
- try:
- mac = uuid.getnode()
- if (mac >> 40) % 2 == 0: # 不是本地伪造MAC
- machine_code = str(mac)
- except Exception as e:
- logger.debug(f"获取MAC地址失败: {e}")
-
- def md5_to_uuid(md5hex):
- # 将32位md5字符串格式化为8-4-4-4-12的UUID格式
- return f"{md5hex[0:8]}-{md5hex[8:12]}-{md5hex[12:16]}-{md5hex[16:20]}-{md5hex[20:32]}"
-
- if machine_code:
- # print(f"machine_code={machine_code!r}") # 可用于调试
- md5 = hashlib.md5(machine_code.encode("utf-8")).hexdigest()
- uuid_str = md5_to_uuid(md5)
- else:
- uuid_str = str(uuid.uuid4())
-
- unique_id = f"{system_info}-{uuid_str}"
- return unique_id
-
-
-def send_heartbeat(server_url, client_id):
- """向服务器发送心跳"""
- sys = platform.system()
- try:
- headers = {"Client-ID": client_id, "User-Agent": f"HeartbeatClient/{client_id[:8]}"}
- data = json.dumps(
- {"system": sys, "Version": global_config.MAI_VERSION},
- )
- logger.debug(f"正在发送心跳到服务器: {server_url}")
- logger.debug(f"心跳数据: {data}")
- response = requests.post(f"{server_url}/api/clients", headers=headers, data=data)
-
- if response.status_code == 201:
- data = response.json()
- logger.debug(f"心跳发送成功。服务器响应: {data}")
- return True
- else:
- logger.debug(f"心跳发送失败。状态码: {response.status_code}, 响应内容: {response.text}")
+ if "deploy_time" not in local_storage:
+ logger.error("本地存储中缺少部署时间,无法请求UUID")
return False
- except requests.RequestException as e:
- # 如果请求异常,可能是网络问题,不记录错误
- logger.debug(f"发送心跳时出错: {e}")
- return False
+ try_count: int = 0
+ while True:
+ # 如果不存在,则向服务端请求一个新的UUID(注册客户端)
+ logger.info("正在向遥测服务端请求UUID...")
+ try:
+ response = requests.post(
+ f"{TELEMETRY_SERVER_URL}/stat/reg_client",
+ json={"deploy_time": local_storage["deploy_time"]},
+ )
-class HeartbeatThread(threading.Thread):
- """心跳线程类"""
+ if response.status_code == 200:
+ data = response.json()
+ client_id = data.get("mmc_uuid")
+ if client_id:
+ # 将UUID存储到本地
+ local_storage["mmc_uuid"] = client_id
+ self.client_uuid = client_id
+ logger.info(f"成功获取UUID: {self.client_uuid}")
+ return True # 成功获取UUID,返回True
+ else:
+ logger.error("无效的服务端响应")
+ else:
+ logger.error(f"请求UUID失败,状态码: {response.status_code}, 响应内容: {response.text}")
+ except requests.RequestException as e:
+ logger.error(f"请求UUID时出错: {e}") # 可能是网络问题
- def __init__(self, server_url, interval):
- super().__init__(daemon=True) # 设置为守护线程,主程序结束时自动结束
- self.server_url = server_url
- self.interval = interval
- self.client_id = get_unique_id()
- self.running = True
- self.stop_event = threading.Event() # 添加事件对象用于可中断的等待
- self.last_heartbeat_time = 0 # 记录上次发送心跳的时间
-
- def run(self):
- """线程运行函数"""
- logger.debug(f"心跳线程已启动,客户端ID: {self.client_id}")
-
- while self.running:
- # 发送心跳
- if send_heartbeat(self.server_url, self.client_id):
- logger.info(f"{self.interval}秒后发送下一次心跳...")
+ # 请求失败,重试次数+1
+ try_count += 1
+ if try_count > 3:
+ # 如果超过3次仍然失败,则退出
+ logger.error("获取UUID失败,请检查网络连接或服务端状态")
+ return False
else:
- logger.info(f"{self.interval}秒后重试...")
+ # 如果可以重试,等待后继续(指数退避)
+ await asyncio.sleep(4**try_count)
- self.last_heartbeat_time = time.time()
+ async def _send_heartbeat(self):
+ """向服务器发送心跳"""
+ try:
+ headers = {
+ "Client-UUID": self.client_uuid,
+ "User-Agent": f"HeartbeatClient/{self.client_uuid[:8]}",
+ }
- # 使用可中断的等待代替 sleep
- # 每秒检查一次是否应该停止或发送心跳
- remaining_wait = self.interval
- while remaining_wait > 0 and self.running:
- # 每次最多等待1秒,便于及时响应停止请求
- wait_time = min(1, remaining_wait)
- if self.stop_event.wait(wait_time):
- break # 如果事件被设置,立即退出等待
- remaining_wait -= wait_time
+ logger.debug(f"正在发送心跳到服务器: {self.server_url}")
- # 检查是否由于外部原因导致间隔异常延长
- if time.time() - self.last_heartbeat_time >= self.interval * 1.5:
- logger.warning("检测到心跳间隔异常延长,立即发送心跳")
- break
+ response = requests.post(
+ f"{self.server_url}/stat/client_heartbeat",
+ headers=headers,
+ json=self.info_dict,
+ )
- def stop(self):
- """停止线程"""
- self.running = False
- self.stop_event.set() # 设置事件,中断等待
- logger.debug("心跳线程已收到停止信号")
+ # 处理响应
+ if 200 <= response.status_code < 300:
+ # 成功
+ logger.debug(f"心跳发送成功,状态码: {response.status_code}")
+ elif response.status_code == 403:
+ # 403 Forbidden
+ logger.error(
+ "心跳发送失败,403 Forbidden: 可能是UUID无效或未注册。"
+ "处理措施:重置UUID,下次发送心跳时将尝试重新注册。"
+ )
+ self.client_uuid = None
+ del local_storage["mmc_uuid"] # 删除本地存储的UUID
+ else:
+ # 其他错误
+ logger.error(f"心跳发送失败,状态码: {response.status_code}, 响应内容: {response.text}")
+ except requests.RequestException as e:
+ logger.error(f"心跳发送失败: {e}")
-def main():
- if global_config.remote_enable:
- """主函数,启动心跳线程"""
- # 配置
- server_url = "http://hyybuth.xyz:10058"
- # server_url = "http://localhost:10058"
- heartbeat_interval = 300 # 5分钟(秒)
+ async def run(self):
+ # 发送心跳
+ if global_config.remote_enable:
+ if self.client_uuid is None:
+ if not await self._req_uuid():
+ logger.error("获取UUID失败,跳过此次心跳")
+ return
- # 创建并启动心跳线程
- heartbeat_thread = HeartbeatThread(server_url, heartbeat_interval)
- heartbeat_thread.start()
-
- return heartbeat_thread # 返回线程对象,便于外部控制
- return None
-
-
-# --- 测试用例 ---
-if __name__ == "__main__":
- print("测试唯一ID生成:")
- print("唯一ID:", get_unique_id())
+ await self._send_heartbeat()
diff --git a/src/plugins/utils/statistic.py b/src/plugins/utils/statistic.py
index b1660d72..b6dac70d 100644
--- a/src/plugins/utils/statistic.py
+++ b/src/plugins/utils/statistic.py
@@ -1,354 +1,836 @@
-import threading
-import time
from collections import defaultdict
from datetime import datetime, timedelta
-from typing import Any, Dict, List
+from typing import Any, Dict, Tuple, List
+
from src.common.logger import get_module_logger
+from src.manager.async_task_manager import AsyncTask
from ...common.database import db
+from src.manager.local_store_manager import local_storage
-logger = get_module_logger("llm_statistics")
+logger = get_module_logger("maibot_statistic")
+
+# 统计数据的键
+TOTAL_REQ_CNT = "total_requests"
+TOTAL_COST = "total_cost"
+REQ_CNT_BY_TYPE = "requests_by_type"
+REQ_CNT_BY_USER = "requests_by_user"
+REQ_CNT_BY_MODEL = "requests_by_model"
+IN_TOK_BY_TYPE = "in_tokens_by_type"
+IN_TOK_BY_USER = "in_tokens_by_user"
+IN_TOK_BY_MODEL = "in_tokens_by_model"
+OUT_TOK_BY_TYPE = "out_tokens_by_type"
+OUT_TOK_BY_USER = "out_tokens_by_user"
+OUT_TOK_BY_MODEL = "out_tokens_by_model"
+TOTAL_TOK_BY_TYPE = "tokens_by_type"
+TOTAL_TOK_BY_USER = "tokens_by_user"
+TOTAL_TOK_BY_MODEL = "tokens_by_model"
+COST_BY_TYPE = "costs_by_type"
+COST_BY_USER = "costs_by_user"
+COST_BY_MODEL = "costs_by_model"
+ONLINE_TIME = "online_time"
+TOTAL_MSG_CNT = "total_messages"
+MSG_CNT_BY_CHAT = "messages_by_chat"
-class LLMStatistics:
- def __init__(self, output_file: str = "llm_statistics.txt"):
- """初始化LLM统计类
+class OnlineTimeRecordTask(AsyncTask):
+ """在线时间记录任务"""
- Args:
- output_file: 统计结果输出文件路径
- """
- self.output_file = output_file
- self.running = False
- self.stats_thread = None
- self.console_thread = None
- self._init_database()
- self.name_dict: Dict[List] = {}
+ def __init__(self):
+ super().__init__(task_name="Online Time Record Task", run_interval=60)
+
+ self.record_id: str | None = None
+ """记录ID"""
+
+ self._init_database() # 初始化数据库
@staticmethod
def _init_database():
- """初始化数据库集合"""
+ """初始化数据库"""
if "online_time" not in db.list_collection_names():
+ # 初始化数据库(在线时长)
db.create_collection("online_time")
- db.online_time.create_index([("timestamp", 1)])
+ # 创建索引
+ if ("end_timestamp", 1) not in db.online_time.list_indexes():
+ db.online_time.create_index([("end_timestamp", 1)])
- def start(self):
- """启动统计线程"""
- if not self.running:
- self.running = True
- # 启动文件统计线程
- self.stats_thread = threading.Thread(target=self._stats_loop)
- self.stats_thread.daemon = True
- self.stats_thread.start()
- # 启动控制台输出线程
- self.console_thread = threading.Thread(target=self._console_output_loop)
- self.console_thread.daemon = True
- self.console_thread.start()
+ async def run(self):
+ try:
+ if self.record_id:
+ # 如果有记录,则更新结束时间
+ db.online_time.update_one(
+ {"_id": self.record_id},
+ {
+ "$set": {
+ "end_timestamp": datetime.now() + timedelta(minutes=1),
+ }
+ },
+ )
+ else:
+ # 如果没有记录,检查一分钟以内是否已有记录
+ current_time = datetime.now()
+ recent_record = db.online_time.find_one(
+ {"end_timestamp": {"$gte": current_time - timedelta(minutes=1)}}
+ )
- def stop(self):
- """停止统计线程"""
- self.running = False
- if self.stats_thread:
- self.stats_thread.join()
- if self.console_thread:
- self.console_thread.join()
+ if not recent_record:
+ # 若没有记录,则插入新的在线时间记录
+ self.record_id = db.online_time.insert_one(
+ {
+ "start_timestamp": current_time,
+ "end_timestamp": current_time + timedelta(minutes=1),
+ }
+ ).inserted_id
+ else:
+ # 如果有记录,则更新结束时间
+ self.record_id = recent_record["_id"]
+ db.online_time.update_one(
+ {"_id": self.record_id},
+ {
+ "$set": {
+ "end_timestamp": current_time + timedelta(minutes=1),
+ }
+ },
+ )
+ except Exception:
+ logger.exception("在线时间记录失败")
+
+
+def _format_online_time(online_seconds: int) -> str:
+ """
+ 格式化在线时间
+ :param online_seconds: 在线时间(秒)
+ :return: 格式化后的在线时间字符串
+ """
+ total_oneline_time = timedelta(seconds=int(online_seconds)) #确保是整数
+
+ days = total_oneline_time.days
+ hours = total_oneline_time.seconds // 3600
+ minutes = (total_oneline_time.seconds // 60) % 60
+ seconds = total_oneline_time.seconds % 60
+ if days > 0:
+ # 如果在线时间超过1天,则格式化为"X天X小时X分钟"
+ total_oneline_time_str = f"{total_oneline_time.days}天{hours}小时{minutes}分钟{seconds}秒"
+ elif hours > 0:
+ # 如果在线时间超过1小时,则格式化为"X小时X分钟X秒"
+ total_oneline_time_str = f"{hours}小时{minutes}分钟{seconds}秒"
+ else:
+ # 其他情况格式化为"X分钟X秒"
+ total_oneline_time_str = f"{minutes}分钟{seconds}秒"
+
+ return total_oneline_time_str
+
+
+class StatisticOutputTask(AsyncTask):
+ """统计输出任务"""
+
+ SEP_LINE = "-" * 84
+
+ def __init__(self, record_file_path: str = "maibot_statistics.html"):
+ # 延迟300秒启动,运行间隔300秒
+ super().__init__(task_name="Statistics Data Output Task", wait_before_start=0, run_interval=300)
+
+ self.name_mapping: Dict[str, Tuple[str, float]] = {}
+ """
+ 联系人/群聊名称映射 {聊天ID: (联系人/群聊名称, 记录时间(timestamp))}
+ 注:设计记录时间的目的是方便更新名称,使联系人/群聊名称保持最新
+ """
+
+ self.record_file_path: str = record_file_path
+ """
+ 记录文件路径
+ """
+
+ now = datetime.now() # Renamed to avoid conflict with 'now' in methods
+ if "deploy_time" in local_storage:
+ # 如果存在部署时间,则使用该时间作为全量统计的起始时间
+ deploy_time = datetime.fromtimestamp(local_storage["deploy_time"])
+ else:
+ # 否则,使用最大时间范围,并记录部署时间为当前时间
+ deploy_time = datetime(2000, 1, 1)
+ local_storage["deploy_time"] = now.timestamp()
+
+ self.stat_period: List[Tuple[str, timedelta, str]] = [
+ ("all_time", now - deploy_time, "自部署以来"), # 必须保留"all_time"
+ ("last_7_days", timedelta(days=7), "最近7天"),
+ ("last_24_hours", timedelta(days=1), "最近24小时"),
+ ("last_hour", timedelta(hours=1), "最近1小时"),
+ ]
+ """
+ 统计时间段 [(统计名称, 统计时间段, 统计描述), ...]
+ """
+
+ def _statistic_console_output(self, stats: Dict[str, Any], now: datetime):
+ """
+ 输出统计数据到控制台
+ :param stats: 统计数据
+ :param now: 基准当前时间
+ """
+ # 输出最近一小时的统计数据
+ last_hour_stats = stats.get("last_hour", {}) # Ensure 'last_hour' key exists
+
+ output = [
+ self.SEP_LINE,
+ f" 最近1小时的统计数据 (自{now.strftime('%Y-%m-%d %H:%M:%S')}开始,详细信息见文件:{self.record_file_path})",
+ self.SEP_LINE,
+ self._format_total_stat(last_hour_stats),
+ "",
+ self._format_model_classified_stat(last_hour_stats),
+ "",
+ self._format_chat_stat(last_hour_stats),
+ self.SEP_LINE,
+ "",
+ ]
+
+ logger.info("\n" + "\n".join(output))
+
+ async def run(self):
+ try:
+ now = datetime.now()
+ # 收集统计数据
+ stats = self._collect_all_statistics(now)
+
+ # 输出统计数据到控制台
+ if "last_hour" in stats: # Check if stats for last_hour were successfully collected
+ self._statistic_console_output(stats, now)
+ else:
+ logger.warning("无法输出最近一小时统计数据到控制台,因为数据缺失。")
+ # 输出统计数据到html文件
+ self._generate_html_report(stats, now)
+ except Exception as e:
+ logger.exception(f"输出统计数据过程中发生异常,错误信息:{e}")
+
+ # -- 以下为统计数据收集方法 --
@staticmethod
- def _record_online_time():
- """记录在线时间"""
- current_time = datetime.now()
- # 检查5分钟内是否已有记录
- recent_record = db.online_time.find_one({"timestamp": {"$gte": current_time - timedelta(minutes=5)}})
-
- if not recent_record:
- db.online_time.insert_one(
- {
- "timestamp": current_time,
- "duration": 5, # 5分钟
- }
- )
-
- def _collect_statistics_for_period(self, start_time: datetime) -> Dict[str, Any]:
- """收集指定时间段的LLM请求统计数据
-
- Args:
- start_time: 统计开始时间
+ def _collect_model_request_for_period(collect_period: List[Tuple[str, datetime]]) -> Dict[str, Any]:
"""
+ 收集指定时间段的LLM请求统计数据
+
+ :param collect_period: 统计时间段 [(period_key, start_datetime), ...]
+ """
+ if not collect_period:
+ return {}
+
+ collect_period.sort(key=lambda x: x[1], reverse=True)
+
stats = {
- "total_requests": 0,
- "requests_by_type": defaultdict(int),
- "requests_by_user": defaultdict(int),
- "requests_by_model": defaultdict(int),
- "average_tokens": 0,
- "total_tokens": 0,
- "total_cost": 0.0,
- "costs_by_user": defaultdict(float),
- "costs_by_type": defaultdict(float),
- "costs_by_model": defaultdict(float),
- # 新增token统计字段
- "tokens_by_type": defaultdict(int),
- "tokens_by_user": defaultdict(int),
- "tokens_by_model": defaultdict(int),
- # 新增在线时间统计
- "online_time_minutes": 0,
- # 新增消息统计字段
- "total_messages": 0,
- "messages_by_user": defaultdict(int),
- "messages_by_chat": defaultdict(int),
+ period_key: {
+ TOTAL_REQ_CNT: 0,
+ REQ_CNT_BY_TYPE: defaultdict(int),
+ REQ_CNT_BY_USER: defaultdict(int),
+ REQ_CNT_BY_MODEL: defaultdict(int),
+ IN_TOK_BY_TYPE: defaultdict(int),
+ IN_TOK_BY_USER: defaultdict(int),
+ IN_TOK_BY_MODEL: defaultdict(int),
+ OUT_TOK_BY_TYPE: defaultdict(int),
+ OUT_TOK_BY_USER: defaultdict(int),
+ OUT_TOK_BY_MODEL: defaultdict(int),
+ TOTAL_TOK_BY_TYPE: defaultdict(int),
+ TOTAL_TOK_BY_USER: defaultdict(int),
+ TOTAL_TOK_BY_MODEL: defaultdict(int),
+ TOTAL_COST: 0.0,
+ COST_BY_TYPE: defaultdict(float),
+ COST_BY_USER: defaultdict(float),
+ COST_BY_MODEL: defaultdict(float),
+ }
+ for period_key, _ in collect_period
}
+
+ # Determine the overall earliest start time for the database query
+ # This assumes collect_period is not empty, which is checked at the beginning.
+ overall_earliest_start_time = min(p[1] for p in collect_period)
- cursor = db.llm_usage.find({"timestamp": {"$gte": start_time}})
- total_requests = 0
+ for record in db.llm_usage.find({"timestamp": {"$gte": overall_earliest_start_time}}):
+ record_timestamp = record.get("timestamp")
+ if not isinstance(record_timestamp, datetime): # Ensure timestamp is a datetime object
+ try: # Attempt conversion if it's a number (e.g. Unix timestamp)
+ record_timestamp = datetime.fromtimestamp(float(record_timestamp))
+ except (ValueError, TypeError):
+ logger.warning(f"Skipping LLM usage record with invalid timestamp: {record.get('_id')}")
+ continue
- for doc in cursor:
- stats["total_requests"] += 1
- request_type = doc.get("request_type", "unknown")
- user_id = str(doc.get("user_id", "unknown"))
- model_name = doc.get("model_name", "unknown")
- stats["requests_by_type"][request_type] += 1
- stats["requests_by_user"][user_id] += 1
- stats["requests_by_model"][model_name] += 1
+ for idx, (current_period_key, period_start_time) in enumerate(collect_period):
+ if record_timestamp >= period_start_time:
+ for period_key_to_update, _ in collect_period[idx:]:
+ stats[period_key_to_update][TOTAL_REQ_CNT] += 1
- prompt_tokens = doc.get("prompt_tokens", 0)
- completion_tokens = doc.get("completion_tokens", 0)
- total_tokens = prompt_tokens + completion_tokens
- stats["tokens_by_type"][request_type] += total_tokens
- stats["tokens_by_user"][user_id] += total_tokens
- stats["tokens_by_model"][model_name] += total_tokens
- stats["total_tokens"] += total_tokens
+ request_type = record.get("request_type", "unknown")
+ user_id = str(record.get("user_id", "unknown"))
+ model_name = record.get("model_name", "unknown")
- cost = doc.get("cost", 0.0)
- stats["total_cost"] += cost
- stats["costs_by_user"][user_id] += cost
- stats["costs_by_type"][request_type] += cost
- stats["costs_by_model"][model_name] += cost
+ stats[period_key_to_update][REQ_CNT_BY_TYPE][request_type] += 1
+ stats[period_key_to_update][REQ_CNT_BY_USER][user_id] += 1
+ stats[period_key_to_update][REQ_CNT_BY_MODEL][model_name] += 1
- total_requests += 1
+ prompt_tokens = record.get("prompt_tokens", 0)
+ completion_tokens = record.get("completion_tokens", 0)
+ total_tokens = prompt_tokens + completion_tokens
- if total_requests > 0:
- stats["average_tokens"] = stats["total_tokens"] / total_requests
+ stats[period_key_to_update][IN_TOK_BY_TYPE][request_type] += prompt_tokens
+ stats[period_key_to_update][IN_TOK_BY_USER][user_id] += prompt_tokens
+ stats[period_key_to_update][IN_TOK_BY_MODEL][model_name] += prompt_tokens
- # 统计在线时间
- online_time_cursor = db.online_time.find({"timestamp": {"$gte": start_time}})
- for doc in online_time_cursor:
- stats["online_time_minutes"] += doc.get("duration", 0)
+ stats[period_key_to_update][OUT_TOK_BY_TYPE][request_type] += completion_tokens
+ stats[period_key_to_update][OUT_TOK_BY_USER][user_id] += completion_tokens
+ stats[period_key_to_update][OUT_TOK_BY_MODEL][model_name] += completion_tokens
- # 统计消息量
- messages_cursor = db.messages.find({"time": {"$gte": start_time.timestamp()}})
- for doc in messages_cursor:
- stats["total_messages"] += 1
- # user_id = str(doc.get("user_info", {}).get("user_id", "unknown"))
- chat_info = doc.get("chat_info", {})
- user_info = doc.get("user_info", {})
- user_id = str(user_info.get("user_id", "unknown"))
- message_time = doc.get("time", 0)
- group_info = chat_info.get("group_info") if chat_info else {}
- # print(f"group_info: {group_info}")
- group_name = None
- if group_info:
- group_id = f"g{group_info.get('group_id')}"
- group_name = group_info.get("group_name", f"群{group_info.get('group_id')}")
- if user_info and not group_name:
- group_id = f"u{user_info['user_id']}"
- group_name = user_info["user_nickname"]
- if self.name_dict.get(group_id):
- if message_time > self.name_dict.get(group_id)[1]:
- self.name_dict[group_id] = [group_name, message_time]
- else:
- self.name_dict[group_id] = [group_name, message_time]
- # print(f"group_name: {group_name}")
- stats["messages_by_user"][user_id] += 1
- stats["messages_by_chat"][group_id] += 1
+ stats[period_key_to_update][TOTAL_TOK_BY_TYPE][request_type] += total_tokens
+ stats[period_key_to_update][TOTAL_TOK_BY_USER][user_id] += total_tokens
+ stats[period_key_to_update][TOTAL_TOK_BY_MODEL][model_name] += total_tokens
+
+ cost = record.get("cost", 0.0)
+ stats[period_key_to_update][TOTAL_COST] += cost
+ stats[period_key_to_update][COST_BY_TYPE][request_type] += cost
+ stats[period_key_to_update][COST_BY_USER][user_id] += cost
+ stats[period_key_to_update][COST_BY_MODEL][model_name] += cost
+ break
return stats
- def _collect_all_statistics(self) -> Dict[str, Dict[str, Any]]:
- """收集所有时间范围的统计数据"""
- now = datetime.now()
- # 使用2000年1月1日作为"所有时间"的起始时间,这是一个更合理的起始点
- all_time_start = datetime(2000, 1, 1)
+ @staticmethod
+ def _collect_online_time_for_period(collect_period: List[Tuple[str, datetime]], now: datetime) -> Dict[str, Any]:
+ """
+ 收集指定时间段的在线时间统计数据
- return {
- "all_time": self._collect_statistics_for_period(all_time_start),
- "last_7_days": self._collect_statistics_for_period(now - timedelta(days=7)),
- "last_24_hours": self._collect_statistics_for_period(now - timedelta(days=1)),
- "last_hour": self._collect_statistics_for_period(now - timedelta(hours=1)),
+ :param collect_period: 统计时间段 [(period_key, start_datetime), ...]
+ :param now: 当前时间,用于校准end_timestamp
+ """
+ if not collect_period:
+ return {}
+
+ collect_period.sort(key=lambda x: x[1], reverse=True)
+
+ stats = {
+ period_key: {
+ ONLINE_TIME: 0.0,
+ }
+ for period_key, _ in collect_period
+ }
+
+ overall_earliest_start_time = min(p[1] for p in collect_period)
+
+ for record in db.online_time.find({"end_timestamp": {"$gte": overall_earliest_start_time}}):
+ record_end_timestamp: datetime = record.get("end_timestamp")
+ record_start_timestamp: datetime = record.get("start_timestamp")
+
+ if not isinstance(record_end_timestamp, datetime) or not isinstance(record_start_timestamp, datetime):
+ logger.warning(f"Skipping online_time record with invalid timestamps: {record.get('_id')}")
+ continue
+
+ actual_end_timestamp = min(record_end_timestamp, now)
+
+ for idx, (current_period_key, period_start_time) in enumerate(collect_period):
+ if record_start_timestamp < now and actual_end_timestamp > period_start_time:
+ overlap_start = max(record_start_timestamp, period_start_time)
+ overlap_end = min(actual_end_timestamp, now)
+
+ if overlap_end > overlap_start:
+ duration_seconds = (overlap_end - overlap_start).total_seconds()
+ for period_key_to_update, _ in collect_period[idx:]:
+ stats[period_key_to_update][ONLINE_TIME] += duration_seconds
+ break
+
+ return stats
+
+ def _collect_message_count_for_period(self, collect_period: List[Tuple[str, datetime]]) -> Dict[str, Any]:
+ """
+ 收集指定时间段的消息统计数据
+
+ :param collect_period: 统计时间段 [(period_key, start_datetime), ...]
+ """
+ if not collect_period:
+ return {}
+
+ collect_period.sort(key=lambda x: x[1], reverse=True)
+
+ stats = {
+ period_key: {
+ TOTAL_MSG_CNT: 0,
+ MSG_CNT_BY_CHAT: defaultdict(int),
+ }
+ for period_key, _ in collect_period
}
- def _format_stats_section(self, stats: Dict[str, Any], title: str) -> str:
- """格式化统计部分的输出"""
- output = ["\n" + "-" * 84, f"{title}", "-" * 84, f"总请求数: {stats['total_requests']}"]
+ overall_earliest_start_timestamp_float = min(p[1].timestamp() for p in collect_period)
+
+ for message in db.messages.find({"time": {"$gte": overall_earliest_start_timestamp_float}}):
+ chat_info = message.get("chat_info", {})
+ user_info = message.get("user_info", {})
+ message_time_ts = message.get("time")
- if stats["total_requests"] > 0:
- output.append(f"总Token数: {stats['total_tokens']}")
- output.append(f"总花费: {stats['total_cost']:.4f}¥")
- output.append(f"在线时间: {stats['online_time_minutes']}分钟")
- output.append(f"总消息数: {stats['total_messages']}\n")
+ if message_time_ts is None:
+ logger.warning(f"Skipping message record with no timestamp: {message.get('_id')}")
+ continue
+
+ try:
+ message_datetime = datetime.fromtimestamp(float(message_time_ts))
+ except (ValueError, TypeError):
+ logger.warning(f"Skipping message record with invalid time format: {message.get('_id')}")
+ continue
- data_fmt = "{:<32} {:>10} {:>14} {:>13.4f} ¥"
- # 按模型统计
- output.append("按模型统计:")
- output.append("模型名称 调用次数 Token总量 累计花费")
- for model_name, count in sorted(stats["requests_by_model"].items()):
- tokens = stats["tokens_by_model"][model_name]
- cost = stats["costs_by_model"][model_name]
- output.append(
- data_fmt.format(model_name[:30] + ".." if len(model_name) > 32 else model_name, count, tokens, cost)
- )
- output.append("")
+ group_info = chat_info.get("group_info")
+ chat_id = None
+ chat_name = None
- # 按请求类型统计
- output.append("按请求类型统计:")
- output.append("模型名称 调用次数 Token总量 累计花费")
- for req_type, count in sorted(stats["requests_by_type"].items()):
- tokens = stats["tokens_by_type"][req_type]
- cost = stats["costs_by_type"][req_type]
- output.append(
- data_fmt.format(req_type[:22] + ".." if len(req_type) > 24 else req_type, count, tokens, cost)
- )
- output.append("")
+ if group_info and group_info.get("group_id"):
+ gid = group_info.get('group_id')
+ chat_id = f"g{gid}"
+ chat_name = group_info.get("group_name", f"群聊 {gid}")
+ elif user_info and user_info.get("user_id"):
+ uid = user_info['user_id']
+ chat_id = f"u{uid}"
+ chat_name = user_info.get("user_nickname", f"用户 {uid}")
+
+ if not chat_id:
+ continue
- # 修正用户统计列宽
- output.append("按用户统计:")
- output.append("用户ID 调用次数 Token总量 累计花费")
- for user_id, count in sorted(stats["requests_by_user"].items()):
- tokens = stats["tokens_by_user"][user_id]
- cost = stats["costs_by_user"][user_id]
- output.append(
- data_fmt.format(
- user_id[:22], # 不再添加省略号,保持原始ID
- count,
- tokens,
- cost,
- )
- )
- output.append("")
+ current_mapping = self.name_mapping.get(chat_id)
+ if current_mapping:
+ if chat_name != current_mapping[0] and message_time_ts > current_mapping[1]:
+ self.name_mapping[chat_id] = (chat_name, message_time_ts)
+ else:
+ self.name_mapping[chat_id] = (chat_name, message_time_ts)
- # 添加聊天统计
- output.append("群组统计:")
- output.append("群组名称 消息数量")
- for group_id, count in sorted(stats["messages_by_chat"].items()):
- output.append(f"{self.name_dict[group_id][0][:32]:<32} {count:>10}")
- return "\n".join(output)
+ for idx, (current_period_key, period_start_time) in enumerate(collect_period):
+ if message_datetime >= period_start_time:
+ for period_key_to_update, _ in collect_period[idx:]:
+ stats[period_key_to_update][TOTAL_MSG_CNT] += 1
+ stats[period_key_to_update][MSG_CNT_BY_CHAT][chat_id] += 1
+ break
- def _format_stats_section_lite(self, stats: Dict[str, Any], title: str) -> str:
- """格式化统计部分的输出"""
- output = ["\n" + "-" * 84, f"{title}", "-" * 84]
+ return stats
- # output.append(f"总请求数: {stats['total_requests']}")
- if stats["total_requests"] > 0:
- # output.append(f"总Token数: {stats['total_tokens']}")
- output.append(f"总花费: {stats['total_cost']:.4f}¥")
- # output.append(f"在线时间: {stats['online_time_minutes']}分钟")
- output.append(f"总消息数: {stats['total_messages']}\n")
+ def _collect_all_statistics(self, now: datetime) -> Dict[str, Dict[str, Any]]:
+ """
+ 收集各时间段的统计数据
+ :param now: 基准当前时间
+ """
+ # Correctly determine deploy_time
+ if "deploy_time" in local_storage:
+ try:
+ deploy_time = datetime.fromtimestamp(local_storage["deploy_time"])
+ except (TypeError, ValueError):
+ logger.error("Invalid deploy_time in local_storage. Resetting.")
+ deploy_time = datetime(2000, 1, 1)
+ local_storage["deploy_time"] = now.timestamp()
+ else:
+ deploy_time = datetime(2000, 1, 1)
+ local_storage["deploy_time"] = now.timestamp()
- data_fmt = "{:<32} {:>10} {:>14} {:>13.4f} ¥"
-
- # 按模型统计
- output.append("按模型统计:")
- output.append("模型名称 调用次数 Token总量 累计花费")
- for model_name, count in sorted(stats["requests_by_model"].items()):
- tokens = stats["tokens_by_model"][model_name]
- cost = stats["costs_by_model"][model_name]
- output.append(
- data_fmt.format(model_name[:30] + ".." if len(model_name) > 32 else model_name, count, tokens, cost)
- )
- output.append("")
-
- # 按请求类型统计
- # output.append("按请求类型统计:")
- # output.append(("模型名称 调用次数 Token总量 累计花费"))
- # for req_type, count in sorted(stats["requests_by_type"].items()):
- # tokens = stats["tokens_by_type"][req_type]
- # cost = stats["costs_by_type"][req_type]
- # output.append(
- # data_fmt.format(req_type[:22] + ".." if len(req_type) > 24 else req_type, count, tokens, cost)
- # )
- # output.append("")
-
- # 修正用户统计列宽
- # output.append("按用户统计:")
- # output.append(("用户ID 调用次数 Token总量 累计花费"))
- # for user_id, count in sorted(stats["requests_by_user"].items()):
- # tokens = stats["tokens_by_user"][user_id]
- # cost = stats["costs_by_user"][user_id]
- # output.append(
- # data_fmt.format(
- # user_id[:22], # 不再添加省略号,保持原始ID
- # count,
- # tokens,
- # cost,
- # )
- # )
- # output.append("")
-
- # 添加聊天统计
- output.append("群组统计:")
- output.append("群组名称 消息数量")
- for group_id, count in sorted(stats["messages_by_chat"].items()):
- output.append(f"{self.name_dict[group_id][0][:32]:<32} {count:>10}")
-
- return "\n".join(output)
-
- def _save_statistics(self, all_stats: Dict[str, Dict[str, Any]]):
- """将统计结果保存到文件"""
- current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-
- output = [f"LLM请求统计报告 (生成时间: {current_time})"]
-
- # 添加各个时间段的统计
- sections = [
- ("所有时间统计", "all_time"),
- ("最近7天统计", "last_7_days"),
- ("最近24小时统计", "last_24_hours"),
- ("最近1小时统计", "last_hour"),
+ # Rebuild stat_period based on the current 'now' and determined 'deploy_time'
+ current_stat_periods_config = [
+ ("all_time", now - deploy_time if now > deploy_time else timedelta(seconds=0), "自部署以来"),
+ ("last_7_days", timedelta(days=7), "最近7天"),
+ ("last_24_hours", timedelta(days=1), "最近24小时"),
+ ("last_hour", timedelta(hours=1), "最近1小时"),
]
+ self.stat_period = current_stat_periods_config # Update instance's stat_period if needed elsewhere
- for title, key in sections:
- output.append(self._format_stats_section(all_stats[key], title))
+ stat_start_timestamp_config = []
+ for period_name, delta, _ in current_stat_periods_config:
+ start_dt = deploy_time if period_name == "all_time" else now - delta
+ stat_start_timestamp_config.append((period_name, start_dt))
- # 写入文件
- with open(self.output_file, "w", encoding="utf-8") as f:
- f.write("\n".join(output))
+ # 收集各类数据
+ model_req_stat = self._collect_model_request_for_period(stat_start_timestamp_config)
+ online_time_stat = self._collect_online_time_for_period(stat_start_timestamp_config, now)
+ message_count_stat = self._collect_message_count_for_period(stat_start_timestamp_config)
- def _console_output_loop(self):
- """控制台输出循环,每5分钟输出一次最近1小时的统计"""
- while self.running:
- # 等待5分钟
- for _ in range(300): # 5分钟 = 300秒
- if not self.running:
- break
- time.sleep(1)
+ final_stats = {}
+ for period_key, _ in stat_start_timestamp_config:
+ final_stats[period_key] = {}
+ final_stats[period_key].update(model_req_stat.get(period_key, {}))
+ final_stats[period_key].update(online_time_stat.get(period_key, {}))
+ final_stats[period_key].update(message_count_stat.get(period_key, {}))
+
+ for stat_field_key in [
+ TOTAL_REQ_CNT, REQ_CNT_BY_TYPE, REQ_CNT_BY_USER, REQ_CNT_BY_MODEL,
+ IN_TOK_BY_TYPE, IN_TOK_BY_USER, IN_TOK_BY_MODEL,
+ OUT_TOK_BY_TYPE, OUT_TOK_BY_USER, OUT_TOK_BY_MODEL,
+ TOTAL_TOK_BY_TYPE, TOTAL_TOK_BY_USER, TOTAL_TOK_BY_MODEL,
+ TOTAL_COST, COST_BY_TYPE, COST_BY_USER, COST_BY_MODEL,
+ ONLINE_TIME, TOTAL_MSG_CNT, MSG_CNT_BY_CHAT
+ ]:
+ if stat_field_key not in final_stats[period_key]:
+ # Initialize with appropriate default type if key is missing
+ if "BY_" in stat_field_key: # These are usually defaultdicts
+ final_stats[period_key][stat_field_key] = defaultdict(int if "CNT" in stat_field_key or "TOK" in stat_field_key else float)
+ elif "CNT" in stat_field_key or "TOK" in stat_field_key :
+ final_stats[period_key][stat_field_key] = 0
+ elif "COST" in stat_field_key or ONLINE_TIME == stat_field_key:
+ final_stats[period_key][stat_field_key] = 0.0
+ return final_stats
+
+ # -- 以下为统计数据格式化方法 --
+
+ @staticmethod
+ def _format_total_stat(stats: Dict[str, Any]) -> str:
+ """
+ 格式化总统计数据
+ """
+ output = [
+ f"总在线时间: {_format_online_time(stats.get(ONLINE_TIME, 0))}",
+ f"总消息数: {stats.get(TOTAL_MSG_CNT, 0)}",
+ f"总请求数: {stats.get(TOTAL_REQ_CNT, 0)}",
+ f"总花费: {stats.get(TOTAL_COST, 0.0):.4f}¥",
+ "",
+ ]
+ return "\n".join(output)
+
+ @staticmethod
+ def _format_model_classified_stat(stats: Dict[str, Any]) -> str:
+ """
+ 格式化按模型分类的统计数据
+ """
+ if stats.get(TOTAL_REQ_CNT, 0) > 0:
+ data_fmt = "{:<32} {:>10} {:>12} {:>12} {:>12} {:>9.4f}¥"
+ output = [
+ "按模型分类统计:",
+ " 模型名称 调用次数 输入Token 输出Token Token总量 累计花费",
+ ]
+ req_cnt_by_model = stats.get(REQ_CNT_BY_MODEL, {})
+ in_tok_by_model = stats.get(IN_TOK_BY_MODEL, defaultdict(int))
+ out_tok_by_model = stats.get(OUT_TOK_BY_MODEL, defaultdict(int))
+ total_tok_by_model = stats.get(TOTAL_TOK_BY_MODEL, defaultdict(int))
+ cost_by_model = stats.get(COST_BY_MODEL, defaultdict(float))
+
+ for model_name, count in sorted(req_cnt_by_model.items()):
+ name = model_name[:29] + "..." if len(model_name) > 32 else model_name
+ in_tokens = in_tok_by_model[model_name]
+ out_tokens = out_tok_by_model[model_name]
+ tokens = total_tok_by_model[model_name]
+ cost = cost_by_model[model_name]
+ output.append(data_fmt.format(name, count, in_tokens, out_tokens, tokens, cost))
+
+ output.append("")
+ return "\n".join(output)
+ else:
+ return ""
+
+ def _format_chat_stat(self, stats: Dict[str, Any]) -> str:
+ """
+ 格式化聊天统计数据
+ """
+ if stats.get(TOTAL_MSG_CNT, 0) > 0:
+ output = ["聊天消息统计:", " 联系人/群组名称 消息数量"]
+ msg_cnt_by_chat = stats.get(MSG_CNT_BY_CHAT, {})
+ for chat_id, count in sorted(msg_cnt_by_chat.items()):
+ chat_name_display = self.name_mapping.get(chat_id, (f"未知 ({chat_id})", None))[0]
+ output.append(f"{chat_name_display[:32]:<32} {count:>10}")
+
+ output.append("")
+ return "\n".join(output)
+ else:
+ return ""
+
+ def _generate_html_report(self, stat_collection: dict[str, Any], now: datetime):
+ """
+ 生成HTML格式的统计报告
+ :param stat_collection: 包含所有时间段统计数据的字典 {period_key: stats_dict}
+ :param now: 基准当前时间
+ """
+ # Correctly get deploy_time_dt for display purposes
+ if "deploy_time" in local_storage:
try:
- # 收集最近1小时的统计数据
- now = datetime.now()
- hour_stats = self._collect_statistics_for_period(now - timedelta(hours=1))
+ deploy_time_dt = datetime.fromtimestamp(local_storage["deploy_time"])
+ except (TypeError, ValueError):
+ logger.error("Invalid deploy_time in local_storage for HTML report. Using default.")
+ deploy_time_dt = datetime(2000,1,1) # Fallback
+ else:
+ # This should ideally not happen if __init__ or _collect_all_statistics ran
+ logger.warning("deploy_time not found in local_storage for HTML report. Using default.")
+ deploy_time_dt = datetime(2000, 1, 1) # Fallback
- # 使用logger输出
- stats_output = self._format_stats_section_lite(
- hour_stats, "最近1小时统计:详细信息见根目录文件:llm_statistics.txt"
- )
- logger.info("\n" + stats_output + "\n" + "=" * 50)
+ tab_list_html = []
+ tab_content_html_list = []
- except Exception:
- logger.exception("控制台统计数据输出失败")
+ for period_key, period_delta, period_display_name in self.stat_period: # Use self.stat_period as defined by _collect_all_statistics
+ tab_list_html.append(
+ f''
+ )
- def _stats_loop(self):
- """统计循环,每5分钟运行一次"""
- while self.running:
- try:
- # 记录在线时间
- self._record_online_time()
- # 收集并保存统计数据
- all_stats = self._collect_all_statistics()
- self._save_statistics(all_stats)
- except Exception:
- logger.exception("统计数据处理失败")
+ current_period_stats = stat_collection.get(period_key, {})
+
+ if period_key == "all_time":
+ start_time_dt_for_period = deploy_time_dt
+ else:
+ # Ensure period_delta is a timedelta object
+ if isinstance(period_delta, timedelta):
+ start_time_dt_for_period = now - period_delta
+ else: # Fallback if period_delta is not as expected (e.g. from old self.stat_period)
+ logger.warning(f"period_delta for {period_key} is not a timedelta. Using 'now'. Type: {type(period_delta)}")
+ start_time_dt_for_period = now
+
+
+ html_content_for_tab = f"""
+
+
+ 统计时段:
+ {start_time_dt_for_period.strftime("%Y-%m-%d %H:%M:%S")} ~ {now.strftime("%Y-%m-%d %H:%M:%S")}
+
+
总在线时间: {_format_online_time(current_period_stats.get(ONLINE_TIME, 0))}
+
总消息数: {current_period_stats.get(TOTAL_MSG_CNT, 0)}
+
总请求数: {current_period_stats.get(TOTAL_REQ_CNT, 0)}
+
总花费: {current_period_stats.get(TOTAL_COST, 0.0):.4f} ¥
+ """
+
+ html_content_for_tab += "
按模型分类统计
| 模型名称 | 调用次数 | 输入Token | 输出Token | Token总量 | 累计花费 |
"
+ req_cnt_by_model = current_period_stats.get(REQ_CNT_BY_MODEL, {})
+ in_tok_by_model = current_period_stats.get(IN_TOK_BY_MODEL, defaultdict(int))
+ out_tok_by_model = current_period_stats.get(OUT_TOK_BY_MODEL, defaultdict(int))
+ total_tok_by_model = current_period_stats.get(TOTAL_TOK_BY_MODEL, defaultdict(int))
+ cost_by_model = current_period_stats.get(COST_BY_MODEL, defaultdict(float))
+ if req_cnt_by_model:
+ for model_name, count in sorted(req_cnt_by_model.items()):
+ html_content_for_tab += (
+ f""
+ f"| {model_name} | "
+ f"{count} | "
+ f"{in_tok_by_model[model_name]} | "
+ f"{out_tok_by_model[model_name]} | "
+ f"{total_tok_by_model[model_name]} | "
+ f"{cost_by_model[model_name]:.4f} ¥ | "
+ f"
"
+ )
+ else:
+ html_content_for_tab += "| 无数据 |
"
+ html_content_for_tab += "
"
+
+ html_content_for_tab += "
按请求类型分类统计
| 请求类型 | 调用次数 | 输入Token | 输出Token | Token总量 | 累计花费 |
"
+ req_cnt_by_type = current_period_stats.get(REQ_CNT_BY_TYPE, {})
+ in_tok_by_type = current_period_stats.get(IN_TOK_BY_TYPE, defaultdict(int))
+ out_tok_by_type = current_period_stats.get(OUT_TOK_BY_TYPE, defaultdict(int))
+ total_tok_by_type = current_period_stats.get(TOTAL_TOK_BY_TYPE, defaultdict(int))
+ cost_by_type = current_period_stats.get(COST_BY_TYPE, defaultdict(float))
+ if req_cnt_by_type:
+ for req_type, count in sorted(req_cnt_by_type.items()):
+ html_content_for_tab += (
+ f""
+ f"| {req_type} | "
+ f"{count} | "
+ f"{in_tok_by_type[req_type]} | "
+ f"{out_tok_by_type[req_type]} | "
+ f"{total_tok_by_type[req_type]} | "
+ f"{cost_by_type[req_type]:.4f} ¥ | "
+ f"
"
+ )
+ else:
+ html_content_for_tab += "| 无数据 |
"
+ html_content_for_tab += "
"
+
+ html_content_for_tab += "
按用户分类统计
| 用户ID/名称 | 调用次数 | 输入Token | 输出Token | Token总量 | 累计花费 |
"
+ req_cnt_by_user = current_period_stats.get(REQ_CNT_BY_USER, {})
+ in_tok_by_user = current_period_stats.get(IN_TOK_BY_USER, defaultdict(int))
+ out_tok_by_user = current_period_stats.get(OUT_TOK_BY_USER, defaultdict(int))
+ total_tok_by_user = current_period_stats.get(TOTAL_TOK_BY_USER, defaultdict(int))
+ cost_by_user = current_period_stats.get(COST_BY_USER, defaultdict(float))
+ if req_cnt_by_user:
+ for user_id, count in sorted(req_cnt_by_user.items()):
+ user_display_name = self.name_mapping.get(user_id, (user_id, None))[0]
+ html_content_for_tab += (
+ f""
+ f"| {user_display_name} | "
+ f"{count} | "
+ f"{in_tok_by_user[user_id]} | "
+ f"{out_tok_by_user[user_id]} | "
+ f"{total_tok_by_user[user_id]} | "
+ f"{cost_by_user[user_id]:.4f} ¥ | "
+ f"
"
+ )
+ else:
+ html_content_for_tab += "| 无数据 |
"
+ html_content_for_tab += "
"
+
+ html_content_for_tab += "
聊天消息统计
| 联系人/群组名称 | 消息数量 |
"
+ msg_cnt_by_chat = current_period_stats.get(MSG_CNT_BY_CHAT, {})
+ if msg_cnt_by_chat:
+ for chat_id, count in sorted(msg_cnt_by_chat.items()):
+ chat_name_display = self.name_mapping.get(chat_id, (f"未知/归档聊天 ({chat_id})", None))[0]
+ html_content_for_tab += f"| {chat_name_display} | {count} |
"
+ else:
+ html_content_for_tab += "| 无数据 |
"
+ html_content_for_tab += "
"
+
+ tab_content_html_list.append(html_content_for_tab)
+
+
+ html_template = (
+ """
+
+
+
+
+
+ MaiBot运行统计报告
+
+
+
+"""
+ + f"""
+
+
MaiBot运行统计报告
+
统计截止时间: {now.strftime("%Y-%m-%d %H:%M:%S")}
+
+
+ {"".join(tab_list_html)}
+
+
+ {"".join(tab_content_html_list)}
+
+
+
+"""
+ + """
+
+
+
+ """
+ )
+
+ try:
+ with open(self.record_file_path, "w", encoding="utf-8") as f:
+ f.write(html_template)
+ logger.info(f"统计报告已生成: {self.record_file_path}")
+ except IOError as e:
+ logger.error(f"无法写入统计报告文件 {self.record_file_path}: {e}")
- # 等待5分钟
- for _ in range(300): # 5分钟 = 300秒
- if not self.running:
- break
- time.sleep(1)
diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml
index c107adbf..1ed9e034 100644
--- a/template/bot_config_template.toml
+++ b/template/bot_config_template.toml
@@ -1,5 +1,5 @@
[inner]
-version = "1.6.2.3"
+version = "1.6.2.4"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件,请在修改后将version的值进行变更
@@ -130,8 +130,9 @@ enable_nickname_mapping = false # 绰号映射功能总开关(默认关闭,
max_nicknames_in_prompt = 10 # Prompt 中最多注入的绰号数量(防止token数量爆炸)
nickname_probability_smoothing = 1 # 绰号加权随机选择的平滑因子
nickname_queue_max_size = 100 # 绰号处理队列最大容量
-nickname_process_sleep_interval = 5 # 绰号处理进程休眠间隔(秒)
+nickname_process_sleep_interval = 5 # 绰号处理进程休眠间隔(秒),不建议超过5,否则大概率导致结束过程中超时
nickname_analysis_history_limit = 30 # 绰号处理可见最大上下文
+nickname_analysis_probability = 0.1 # 绰号随机概率命中,该值越大,绰号分析越频繁
[memory]
build_memory_interval = 2000 # 记忆构建间隔 单位秒 间隔越低,麦麦学习越多,但是冗余信息也会增多
@@ -149,7 +150,7 @@ consolidation_similarity_threshold = 0.7 # 相似度阈值
consolidation_check_percentage = 0.01 # 检查节点比例
#不希望记忆的词,已经记忆的不会受到影响
-memory_ban_words = [
+memory_ban_words = [
# "403","张三"
]
@@ -194,19 +195,43 @@ enable_kaomoji_protection = false # 是否启用颜文字保护
model_max_output_length = 256 # 模型单次返回的最大token数
[remote] #发送统计信息,主要是看全球有多少只麦麦
-enable = true
+enable = false
[experimental] #实验性功能
-enable_friend_chat = false # 是否启用好友聊天
+enable_friend_chat = true # 是否启用好友聊天
+enable_friend_whitelist = true # 是否启用好友聊天白名单
talk_allowed_private = [] # 可以回复消息的QQ号
-pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立
-api_polling_max_retries = 3
-enable_pfc_reply_checker = true # 是否启用 PFC 的回复检查器
-pfc_message_buffer_size = 2 # PFC 聊天消息缓冲数量,有利于使聊天节奏更加紧凑流畅,请根据实际 LLM 响应速度进行调整,默认2条
+api_polling_max_retries = 3 # 神秘小功能
rename_person = true # 是否启用改名工具,可以让麦麦对唯一名进行更改,可能可以更拟人地称呼他人,但是也可能导致记忆混淆的问题
-[idle_chat]
-enable_idle_chat = false # 是否启用 pfc 主动发言
+[pfc]
+enable_pfc_chatting = true # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立
+pfc_message_buffer_size = 2 # PFC 聊天消息缓冲数量,有利于使聊天节奏更加紧凑流畅,请根据实际 LLM 响应速度进行调整,默认2条
+pfc_recent_history_display_count = 20 # PFC 对话最大可见上下文
+
+[[pfc.checker]]
+enable_pfc_reply_checker = true # 是否启用 PFC 的回复检查器
+pfc_max_reply_attempts = 3 # 发言最多尝试次数
+pfc_max_chat_history_for_checker = 50 # checker聊天记录最大可见上文长度
+
+[[pfc.emotion]]
+pfc_emotion_update_intensity = 0.6 # 情绪更新强度
+pfc_emotion_history_count = 5 # 情绪更新最大可见上下文长度
+
+[[pfc.relationship]]
+pfc_relationship_incremental_interval = 10 # 关系值增值强度
+pfc_relationship_incremental_msg_count = 10 # 会话中,关系值判断最大可见上下文
+pfc_relationship_incremental_default_change = 1.0 # 会话中,关系值默认更新值(当 llm 返回错误时默认采用该值)
+pfc_relationship_incremental_max_change = 5.0 # 会话中,关系值最大可变值
+pfc_relationship_final_msg_count = 30 # 会话结束时,关系值判断最大可见上下文
+pfc_relationship_final_default_change =5.0 # 会话结束时,关系值默认更新值
+pfc_relationship_final_max_change = 50.0 # 会话结束时,关系值最大可变值
+
+[[pfc.fallback]]
+pfc_historical_fallback_exclude_seconds = 7200 # pfc 翻看聊天记录排除最近时长
+
+[[pfc.idle_chat]]
+enable_idle_chat = true # 是否启用 pfc 主动发言
idle_check_interval = 10 # 检查间隔,10分钟检查一次
min_cooldown = 7200 # 最短冷却时间,2小时 (7200秒)
max_cooldown = 18000 # 最长冷却时间,5小时 (18000秒)
@@ -306,14 +331,6 @@ temp = 0.3
pri_in = 2
pri_out = 8
-# PFC 关系评估LLM
-[model.llm_PFC_relationship_eval]
-name = "Pro/deepseek-ai/DeepSeek-V3" # 或者其他你认为适合判断任务的模型
-provider = "SILICONFLOW"
-temp = 0.4
-pri_in = 2
-pri_out = 8
-
#绰号映射生成模型
[model.llm_nickname_mapping]
name = "Qwen/Qwen2.5-32B-Instruct"
@@ -338,6 +355,16 @@ temp = 0.3
pri_in = 2
pri_out = 8
+# PFC 关系评估LLM
+[model.llm_PFC_relationship_eval]
+name = "deepseek-ai/DeepSeek-V3"
+provider = "SILICONFLOW"
+temp = 0.4
+max_tokens = 512
+pri_in = 2
+pri_out = 8
+
+
#以下模型暂时没有使用!!
#以下模型暂时没有使用!!
#以下模型暂时没有使用!!