pull/937/head
Bakadax 2025-05-16 15:30:19 +08:00
parent b16135a6aa
commit 505aa839b6
15 changed files with 851 additions and 2173 deletions

View File

@ -81,7 +81,7 @@ class ChattingObservation(Observation):
mid_memory_str = ""
if ids:
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:

View File

@ -284,7 +284,7 @@ class SubHeartflowManager:
return # 如果不允许,直接返回
# --- 结束新增 ---
logger.debug(f"当前状态 ({current_state.value}) 可以在{focused_limit}个群 专注聊天")
logger.info(f"当前状态 ({current_state.value}) 可以在{focused_limit}个群 专注聊天")
if focused_limit <= 0:
# logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 不允许 FOCUSED 子心流")
@ -402,7 +402,7 @@ class SubHeartflowManager:
_mai_state_description = f"你当前状态: {current_mai_state.value}"
individuality = Individuality.get_instance()
personality_prompt = individuality.get_prompt(x_person=2, level=3)
prompt_personality = f"{individuality.name}{personality_prompt}"
prompt_personality = f"正在扮演名为{individuality.name}的人类{personality_prompt}"
# --- 修改:在 prompt 中加入当前聊天计数和群名信息 (条件显示) ---
chat_status_lines = []

View File

@ -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
logger.debug(f"找到相关关系,相似度:{(res[1] * 100):.2f}% - {rel_str}")
print(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.debug("找到相关关系将使用RAG进行检索")
logger.info("找到相关关系将使用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.debug(f"RAG检索用时{part_end_time - part_start_time:.5f}s")
logger.infoinfo(f"RAG检索用时{part_end_time - part_start_time:.5f}s")
else:
logger.debug("未找到相关关系,将使用文段检索结果")
logger.infoinfo("未找到相关关系,将使用文段检索结果")
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
logger.debug(f"找到相关文段,相关系数:{res[1]:.8f}\n{raw_paragraph}\n\n")
logger.info(f"找到相关文段,相关系数:{res[1]:.8f}\n{raw_paragraph}\n\n")
return result, ppr_node_weights
else:

View File

@ -1,4 +1,3 @@
# TODO: 原生多模态支持
import time
from abc import abstractmethod
from dataclasses import dataclass

File diff suppressed because it is too large Load Diff

View File

@ -205,7 +205,7 @@ class PersonInfoManager:
existing_names = ""
while current_try < max_retries:
individuality = Individuality.get_instance()
prompt_personality = individuality.get_prompt(x_person=2, level=3)
prompt_personality = individuality.get_prompt(x_person=2, level=1)
bot_name = individuality.personality.bot_nickname
qv_name_prompt = f"你是{bot_name}{prompt_personality}"

View File

@ -313,7 +313,7 @@ class RelationshipManager:
value = self.mood_feedback(value)
level_num = self.calculate_level_num(old_value + value)
relationship_level = ["厌恶", "冷漠", "一般", "友好", "喜欢", "依赖"]
relationship_level = ["厌恶", "冷漠", "一般", "友好", "喜欢", "暧昧"]
logger.info(
f"用户: {user_info.user_nickname}"
f"当前关系: {relationship_level[level_num]}, "
@ -400,7 +400,7 @@ class RelationshipManager:
value = self.mood_feedback(value)
level_num = self.calculate_level_num(old_value + value)
relationship_level = ["厌恶", "冷漠", "一般", "友好", "喜欢", "依赖"]
relationship_level = ["厌恶", "冷漠", "一般", "友好", "喜欢", "暧昧"]
logger.info(
f"用户: {chat_stream.user_info.user_nickname}"
f"当前关系: {relationship_level[level_num]}, "
@ -425,7 +425,7 @@ class RelationshipManager:
level_num = self.calculate_level_num(relationship_value)
if level_num == 0 or level_num == 5:
relationship_level = ["厌恶", "冷漠以对", "认识", "友好对待", "喜欢", "依赖"]
relationship_level = ["厌恶", "冷漠以对", "认识", "友好对待", "喜欢", "暧昧"]
relation_prompt2_list = [
"忽视的回应",
"冷淡回复",
@ -439,7 +439,7 @@ class RelationshipManager:
return ""
else:
if random.random() < 0.6:
relationship_level = ["厌恶", "冷漠以对", "认识", "友好对待", "喜欢", "依赖"]
relationship_level = ["厌恶", "冷漠以对", "认识", "友好对待", "喜欢", "暧昧"]
relation_prompt2_list = [
"忽视的回应",
"冷淡回复",

View File

@ -69,19 +69,9 @@ class OnlineTimeRecordTask(AsyncTask):
else:
# 如果没有记录,检查一分钟以内是否已有记录
current_time = datetime.now()
recent_record = db.online_time.find_one(
if recent_record := db.online_time.find_one(
{"end_timestamp": {"$gte": current_time - timedelta(minutes=1)}}
)
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(
@ -92,8 +82,16 @@ class OnlineTimeRecordTask(AsyncTask):
}
},
)
except Exception:
logger.exception("在线时间记录失败")
else:
# 若没有记录,则插入新的在线时间记录
self.record_id = db.online_time.insert_one(
{
"start_timestamp": current_time,
"end_timestamp": current_time + timedelta(minutes=1),
}
).inserted_id
except Exception as e:
logger.error(f"在线时间记录失败,错误信息:{e}")
def _format_online_time(online_seconds: int) -> str:
@ -102,7 +100,7 @@ def _format_online_time(online_seconds: int) -> str:
:param online_seconds: 在线时间
:return: 格式化后的在线时间字符串
"""
total_oneline_time = timedelta(seconds=int(online_seconds)) # 确保是整数
total_oneline_time = timedelta(seconds=online_seconds)
days = total_oneline_time.days
hours = total_oneline_time.seconds // 3600
@ -110,15 +108,13 @@ def _format_online_time(online_seconds: int) -> str:
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}"
return f"{total_oneline_time.days}{hours}小时{minutes}分钟{seconds}"
elif hours > 0:
# 如果在线时间超过1小时则格式化为"X小时X分钟X秒"
total_oneline_time_str = f"{hours}小时{minutes}分钟{seconds}"
return f"{hours}小时{minutes}分钟{seconds}"
else:
# 其他情况格式化为"X分钟X秒"
total_oneline_time_str = f"{minutes}分钟{seconds}"
return total_oneline_time_str
return f"{minutes}分钟{seconds}"
class StatisticOutputTask(AsyncTask):
@ -141,7 +137,7 @@ class StatisticOutputTask(AsyncTask):
记录文件路径
"""
now = datetime.now() # Renamed to avoid conflict with 'now' in methods
now = datetime.now()
if "deploy_time" in local_storage:
# 如果存在部署时间,则使用该时间作为全量统计的起始时间
deploy_time = datetime.fromtimestamp(local_storage["deploy_time"])
@ -167,17 +163,16 @@ class StatisticOutputTask(AsyncTask):
: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_total_stat(stats["last_hour"]),
"",
self._format_model_classified_stat(last_hour_stats),
self._format_model_classified_stat(stats["last_hour"]),
"",
self._format_chat_stat(last_hour_stats),
self._format_chat_stat(stats["last_hour"]),
self.SEP_LINE,
"",
]
@ -191,10 +186,7 @@ class StatisticOutputTask(AsyncTask):
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("无法输出最近一小时统计数据到控制台,因为数据缺失。")
self._statistic_console_output(stats, now)
# 输出统计数据到html文件
self._generate_html_report(stats, now)
except Exception as e:
@ -207,29 +199,37 @@ class StatisticOutputTask(AsyncTask):
"""
收集指定时间段的LLM请求统计数据
:param collect_period: 统计时间段 [(period_key, start_datetime), ...]
:param collect_period: 统计时间段
"""
if not collect_period:
if len(collect_period) <= 0:
return {}
collect_period.sort(key=lambda x: x[1], reverse=True)
else:
# 排序-按照时间段开始时间降序排列(最晚的时间段在前)
collect_period.sort(key=lambda x: x[1], reverse=True)
stats = {
period_key: {
# 总LLM请求数
TOTAL_REQ_CNT: 0,
# 请求次数统计
REQ_CNT_BY_TYPE: defaultdict(int),
REQ_CNT_BY_USER: defaultdict(int),
REQ_CNT_BY_MODEL: defaultdict(int),
# 输入Token数
IN_TOK_BY_TYPE: defaultdict(int),
IN_TOK_BY_USER: defaultdict(int),
IN_TOK_BY_MODEL: defaultdict(int),
# 输出Token数
OUT_TOK_BY_TYPE: defaultdict(int),
OUT_TOK_BY_USER: defaultdict(int),
OUT_TOK_BY_MODEL: defaultdict(int),
# 总Token数
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),
@ -237,54 +237,46 @@ class StatisticOutputTask(AsyncTask):
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)
for record in db.llm_usage.find({"timestamp": {"$gte": overall_earliest_start_time}}):
# 以最早的时间戳为起始时间获取记录
for record in db.llm_usage.find({"timestamp": {"$gte": collect_period[-1][1]}}):
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 idx, (_, period_start) in enumerate(collect_period):
if record_timestamp >= period_start:
# 如果记录时间在当前时间段内,则它一定在更早的时间段内
# 因此,我们可以直接跳过更早的时间段的判断,直接更新当前以及更早时间段的统计数据
for period_key, _ in collect_period[idx:]:
stats[period_key][TOTAL_REQ_CNT] += 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
request_type = record.get("request_type", "unknown") # 请求类型
user_id = str(record.get("user_id", "unknown")) # 用户ID
model_name = record.get("model_name", "unknown") # 模型名称
request_type = record.get("request_type", "unknown")
user_id = str(record.get("user_id", "unknown"))
model_name = record.get("model_name", "unknown")
stats[period_key][REQ_CNT_BY_TYPE][request_type] += 1
stats[period_key][REQ_CNT_BY_USER][user_id] += 1
stats[period_key][REQ_CNT_BY_MODEL][model_name] += 1
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
prompt_tokens = record.get("prompt_tokens", 0) # 输入Token数
completion_tokens = record.get("completion_tokens", 0) # 输出Token数
total_tokens = prompt_tokens + completion_tokens # Token总数 = 输入Token数 + 输出Token数
prompt_tokens = record.get("prompt_tokens", 0)
completion_tokens = record.get("completion_tokens", 0)
total_tokens = prompt_tokens + completion_tokens
stats[period_key][IN_TOK_BY_TYPE][request_type] += prompt_tokens
stats[period_key][IN_TOK_BY_USER][user_id] += prompt_tokens
stats[period_key][IN_TOK_BY_MODEL][model_name] += prompt_tokens
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
stats[period_key][OUT_TOK_BY_TYPE][request_type] += completion_tokens
stats[period_key][OUT_TOK_BY_USER][user_id] += completion_tokens
stats[period_key][OUT_TOK_BY_MODEL][model_name] += completion_tokens
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
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
stats[period_key][TOTAL_TOK_BY_TYPE][request_type] += total_tokens
stats[period_key][TOTAL_TOK_BY_USER][user_id] += total_tokens
stats[period_key][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
stats[period_key][TOTAL_COST] += cost
stats[period_key][COST_BY_TYPE][request_type] += cost
stats[period_key][COST_BY_USER][user_id] += cost
stats[period_key][COST_BY_MODEL][model_name] += cost
break # 取消更早时间段的判断
return stats
@ -293,43 +285,40 @@ class StatisticOutputTask(AsyncTask):
"""
收集指定时间段的在线时间统计数据
:param collect_period: 统计时间段 [(period_key, start_datetime), ...]
:param now: 当前时间用于校准end_timestamp
:param collect_period: 统计时间段
"""
if not collect_period:
if len(collect_period) <= 0:
return {}
collect_period.sort(key=lambda x: x[1], reverse=True)
else:
# 排序-按照时间段开始时间降序排列(最晚的时间段在前)
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
# 统计在线时间
for record in db.online_time.find({"end_timestamp": {"$gte": collect_period[-1][1]}}):
end_timestamp: datetime = record.get("end_timestamp")
for idx, (_, period_start) in enumerate(collect_period):
if end_timestamp >= period_start:
# 由于end_timestamp会超前标记时间所以我们需要判断是否晚于当前时间如果是则使用当前时间作为结束时间
end_timestamp = min(end_timestamp, now)
# 如果记录时间在当前时间段内,则它一定在更早的时间段内
# 因此,我们可以直接跳过更早的时间段的判断,直接更新当前以及更早时间段的统计数据
for period_key, _period_start in collect_period[idx:]:
start_timestamp: datetime = record.get("start_timestamp")
if start_timestamp < _period_start:
# 如果开始时间在查询边界之前,则使用开始时间
stats[period_key][ONLINE_TIME] += (end_timestamp - _period_start).total_seconds()
else:
# 否则,使用开始时间
stats[period_key][ONLINE_TIME] += (end_timestamp - start_timestamp).total_seconds()
break # 取消更早时间段的判断
return stats
@ -337,66 +326,55 @@ class StatisticOutputTask(AsyncTask):
"""
收集指定时间段的消息统计数据
:param collect_period: 统计时间段 [(period_key, start_datetime), ...]
:param collect_period: 统计时间段
"""
if not collect_period:
if len(collect_period) <= 0:
return {}
collect_period.sort(key=lambda x: x[1], reverse=True)
else:
# 排序-按照时间段开始时间降序排列(最晚的时间段在前)
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
}
overall_earliest_start_timestamp_float = min(p[1].timestamp() for p in collect_period)
# 统计消息量
for message in db.messages.find({"time": {"$gte": collect_period[-1][1].timestamp()}}):
chat_info = message.get("chat_info", None) # 聊天信息
user_info = message.get("user_info", None) # 用户信息(消息发送人)
message_time = message.get("time", 0) # 消息时间
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 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
group_info = chat_info.get("group_info")
chat_id = None
chat_name = None
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
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)
group_info = chat_info.get("group_info") if chat_info else None # 尝试获取群聊信息
if group_info is not None:
# 若有群聊信息
chat_id = f"g{group_info.get('group_id')}"
chat_name = group_info.get("group_name", f"{group_info.get('group_id')}")
elif user_info:
# 若没有群聊信息,则尝试获取用户信息
chat_id = f"u{user_info['user_id']}"
chat_name = user_info["user_nickname"]
else:
self.name_mapping[chat_id] = (chat_name, message_time_ts)
continue # 如果没有群组信息也没有用户信息,则跳过
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
if chat_id in self.name_mapping:
if chat_name != self.name_mapping[chat_id][0] and message_time > self.name_mapping[chat_id][1]:
# 如果用户名称不同,且新消息时间晚于之前记录的时间,则更新用户名称
self.name_mapping[chat_id] = (chat_name, message_time)
else:
self.name_mapping[chat_id] = (chat_name, message_time)
for idx, (_, period_start) in enumerate(collect_period):
if message_time >= period_start.timestamp():
# 如果记录时间在当前时间段内,则它一定在更早的时间段内
# 因此,我们可以直接跳过更早的时间段的判断,直接更新当前以及更早时间段的统计数据
for period_key, _ in collect_period[idx:]:
stats[period_key][TOTAL_MSG_CNT] += 1
stats[period_key][MSG_CNT_BY_CHAT][chat_id] += 1
break
return stats
@ -406,77 +384,53 @@ class StatisticOutputTask(AsyncTask):
收集各时间段的统计数据
: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()
# 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
last_all_time_stat = None
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))
if "last_full_statistics" in local_storage:
# 如果存在上次完整统计数据,则使用该数据进行增量统计
last_stat = local_storage["last_full_statistics"] # 上次完整统计数据
# 收集各类数据
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)
self.name_mapping = last_stat["name_mapping"] # 上次完整统计数据的名称映射
last_all_time_stat = last_stat["stat_data"] # 上次完整统计的统计数据
last_stat_timestamp = datetime.fromtimestamp(last_stat["timestamp"]) # 上次完整统计数据的时间戳
self.stat_period = [item for item in self.stat_period if item[0] != "all_time"] # 删除"所有时间"的统计时段
self.stat_period.append(("all_time", now - last_stat_timestamp, "自部署以来的"))
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, {}))
stat_start_timestamp = [(period[0], now - period[1]) for period in self.stat_period]
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
stat = {item[0]: {} for item in self.stat_period}
model_req_stat = self._collect_model_request_for_period(stat_start_timestamp)
online_time_stat = self._collect_online_time_for_period(stat_start_timestamp, now)
message_count_stat = self._collect_message_count_for_period(stat_start_timestamp)
# 统计数据合并
# 合并三类统计数据
for period_key, _ in stat_start_timestamp:
stat[period_key].update(model_req_stat[period_key])
stat[period_key].update(online_time_stat[period_key])
stat[period_key].update(message_count_stat[period_key])
if last_all_time_stat:
# 若存在上次完整统计数据,则将其与当前统计数据合并
for key, val in last_all_time_stat.items():
if isinstance(val, dict):
# 是字典类型,则进行合并
for sub_key, sub_val in val.items():
stat["all_time"][key][sub_key] += sub_val
else:
# 直接合并
stat["all_time"][key] += val
# 更新上次完整统计数据的时间戳
local_storage["last_full_statistics"] = {
"name_mapping": self.name_mapping,
"stat_data": stat["all_time"],
"timestamp": now.timestamp(),
}
return stat
# -- 以下为统计数据格式化方法 --
@ -485,13 +439,15 @@ class StatisticOutputTask(AsyncTask):
"""
格式化总统计数据
"""
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}¥",
f"总在线时间: {_format_online_time(stats[ONLINE_TIME])}",
f"总消息数: {stats[TOTAL_MSG_CNT]}",
f"总请求数: {stats[TOTAL_REQ_CNT]}",
f"总花费: {stats[TOTAL_COST]:.4f}¥",
"",
]
return "\n".join(output)
@staticmethod
@ -499,183 +455,174 @@ class StatisticOutputTask(AsyncTask):
"""
格式化按模型分类的统计数据
"""
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:
if stats[TOTAL_REQ_CNT] <= 0:
return ""
data_fmt = "{:<32} {:>10} {:>12} {:>12} {:>12} {:>9.4f}¥"
output = [
"按模型分类统计:",
" 模型名称 调用次数 输入Token 输出Token Token总量 累计花费",
]
for model_name, count in sorted(stats[REQ_CNT_BY_MODEL].items()):
name = f"{model_name[:29]}..." if len(model_name) > 32 else model_name
in_tokens = stats[IN_TOK_BY_MODEL][model_name]
out_tokens = stats[OUT_TOK_BY_MODEL][model_name]
tokens = stats[TOTAL_TOK_BY_MODEL][model_name]
cost = stats[COST_BY_MODEL][model_name]
output.append(data_fmt.format(name, count, in_tokens, out_tokens, tokens, cost))
output.append("")
return "\n".join(output)
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:
if stats[TOTAL_MSG_CNT] <= 0:
return ""
output = ["聊天消息统计:", " 联系人/群组名称 消息数量"]
output.extend(
f"{self.name_mapping[chat_id][0][:32]:<32} {count:>10}"
for chat_id, count in sorted(stats[MSG_CNT_BY_CHAT].items())
)
output.append("")
return "\n".join(output)
def _generate_html_report(self, stat_collection: dict[str, Any], now: datetime):
def _generate_html_report(self, stat: dict[str, Any], now: datetime):
"""
生成HTML格式的统计报告
:param stat_collection: 包含所有时间段统计数据的字典 {period_key: stats_dict}
:param stat: 统计数据
:param now: 基准当前时间
:return: HTML格式的统计报告
"""
# Correctly get deploy_time_dt for display purposes
if "deploy_time" in local_storage:
try:
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
tab_list_html = []
tab_content_html_list = []
tab_list = [
f'<button class="tab-link" onclick="showTab(event, \'{period[0]}\')">{period[2]}</button>'
for period in self.stat_period
]
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'<button class="tab-link" onclick="showTab(event, \'{period_key}\')">{period_display_name}</button>'
def _format_stat_data(stat_data: dict[str, Any], div_id: str, start_time: datetime) -> str:
"""
格式化一个时间段的统计数据到html div块
:param stat_data: 统计数据
:param div_id: div的ID
:param start_time: 统计时间段开始时间
"""
# format总在线时间
# 按模型分类统计
model_rows = "\n".join(
[
f"<tr>"
f"<td>{model_name}</td>"
f"<td>{count}</td>"
f"<td>{stat_data[IN_TOK_BY_MODEL][model_name]}</td>"
f"<td>{stat_data[OUT_TOK_BY_MODEL][model_name]}</td>"
f"<td>{stat_data[TOTAL_TOK_BY_MODEL][model_name]}</td>"
f"<td>{stat_data[COST_BY_MODEL][model_name]:.4f} ¥</td>"
f"</tr>"
for model_name, count in sorted(stat_data[REQ_CNT_BY_MODEL].items())
]
)
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"""
<div id="{period_key}" class="tab-content">
<p class="info-item">
# 按请求类型分类统计
type_rows = "\n".join(
[
f"<tr>"
f"<td>{req_type}</td>"
f"<td>{count}</td>"
f"<td>{stat_data[IN_TOK_BY_TYPE][req_type]}</td>"
f"<td>{stat_data[OUT_TOK_BY_TYPE][req_type]}</td>"
f"<td>{stat_data[TOTAL_TOK_BY_TYPE][req_type]}</td>"
f"<td>{stat_data[COST_BY_TYPE][req_type]:.4f} ¥</td>"
f"</tr>"
for req_type, count in sorted(stat_data[REQ_CNT_BY_TYPE].items())
]
)
# 按用户分类统计
user_rows = "\n".join(
[
f"<tr>"
f"<td>{user_id}</td>"
f"<td>{count}</td>"
f"<td>{stat_data[IN_TOK_BY_USER][user_id]}</td>"
f"<td>{stat_data[OUT_TOK_BY_USER][user_id]}</td>"
f"<td>{stat_data[TOTAL_TOK_BY_USER][user_id]}</td>"
f"<td>{stat_data[COST_BY_USER][user_id]:.4f} ¥</td>"
f"</tr>"
for user_id, count in sorted(stat_data[REQ_CNT_BY_USER].items())
]
)
# 聊天消息统计
chat_rows = "\n".join(
[
f"<tr><td>{self.name_mapping[chat_id][0]}</td><td>{count}</td></tr>"
for chat_id, count in sorted(stat_data[MSG_CNT_BY_CHAT].items())
]
)
# 生成HTML
return f"""
<div id=\"{div_id}\" class=\"tab-content\">
<p class=\"info-item\">
<strong>统计时段: </strong>
{start_time_dt_for_period.strftime("%Y-%m-%d %H:%M:%S")} ~ {now.strftime("%Y-%m-%d %H:%M:%S")}
{start_time.strftime("%Y-%m-%d %H:%M:%S")} ~ {now.strftime("%Y-%m-%d %H:%M:%S")}
</p>
<p class="info-item"><strong>总在线时间: </strong>{_format_online_time(current_period_stats.get(ONLINE_TIME, 0))}</p>
<p class="info-item"><strong>总消息数: </strong>{current_period_stats.get(TOTAL_MSG_CNT, 0)}</p>
<p class="info-item"><strong>总请求数: </strong>{current_period_stats.get(TOTAL_REQ_CNT, 0)}</p>
<p class="info-item"><strong>总花费: </strong>{current_period_stats.get(TOTAL_COST, 0.0):.4f} ¥</p>
<p class=\"info-item\"><strong>总在线时间: </strong>{_format_online_time(stat_data[ONLINE_TIME])}</p>
<p class=\"info-item\"><strong>总消息数: </strong>{stat_data[TOTAL_MSG_CNT]}</p>
<p class=\"info-item\"><strong>总请求数: </strong>{stat_data[TOTAL_REQ_CNT]}</p>
<p class=\"info-item\"><strong>总花费: </strong>{stat_data[TOTAL_COST]:.4f} ¥</p>
<h2>按模型分类统计</h2>
<table>
<thead><tr><th>模型名称</th><th>调用次数</th><th>输入Token</th><th>输出Token</th><th>Token总量</th><th>累计花费</th></tr></thead>
<tbody>
{model_rows}
</tbody>
</table>
<h2>按请求类型分类统计</h2>
<table>
<thead>
<tr><th>请求类型</th><th>调用次数</th><th>输入Token</th><th>输出Token</th><th>Token总量</th><th>累计花费</th></tr>
</thead>
<tbody>
{type_rows}
</tbody>
</table>
<h2>按用户分类统计</h2>
<table>
<thead>
<tr><th>用户名称</th><th>调用次数</th><th>输入Token</th><th>输出Token</th><th>Token总量</th><th>累计花费</th></tr>
</thead>
<tbody>
{user_rows}
</tbody>
</table>
<h2>聊天消息统计</h2>
<table>
<thead>
<tr><th>联系人/群组名称</th><th>消息数量</th></tr>
</thead>
<tbody>
{chat_rows}
</tbody>
</table>
</div>
"""
html_content_for_tab += "<h2>按模型分类统计</h2><table><thead><tr><th>模型名称</th><th>调用次数</th><th>输入Token</th><th>输出Token</th><th>Token总量</th><th>累计花费</th></tr></thead><tbody>"
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"<tr>"
f"<td>{model_name}</td>"
f"<td>{count}</td>"
f"<td>{in_tok_by_model[model_name]}</td>"
f"<td>{out_tok_by_model[model_name]}</td>"
f"<td>{total_tok_by_model[model_name]}</td>"
f"<td>{cost_by_model[model_name]:.4f} ¥</td>"
f"</tr>"
)
else:
html_content_for_tab += "<tr><td colspan='6'>无数据</td></tr>"
html_content_for_tab += "</tbody></table>"
tab_content_list = [
_format_stat_data(stat[period[0]], period[0], now - period[1])
for period in self.stat_period
if period[0] != "all_time"
]
html_content_for_tab += "<h2>按请求类型分类统计</h2><table><thead><tr><th>请求类型</th><th>调用次数</th><th>输入Token</th><th>输出Token</th><th>Token总量</th><th>累计花费</th></tr></thead><tbody>"
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"<tr>"
f"<td>{req_type}</td>"
f"<td>{count}</td>"
f"<td>{in_tok_by_type[req_type]}</td>"
f"<td>{out_tok_by_type[req_type]}</td>"
f"<td>{total_tok_by_type[req_type]}</td>"
f"<td>{cost_by_type[req_type]:.4f} ¥</td>"
f"</tr>"
)
else:
html_content_for_tab += "<tr><td colspan='6'>无数据</td></tr>"
html_content_for_tab += "</tbody></table>"
tab_content_list.append(
_format_stat_data(stat["all_time"], "all_time", datetime.fromtimestamp(local_storage["deploy_time"]))
)
html_content_for_tab += "<h2>按用户分类统计</h2><table><thead><tr><th>用户ID/名称</th><th>调用次数</th><th>输入Token</th><th>输出Token</th><th>Token总量</th><th>累计花费</th></tr></thead><tbody>"
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"<tr>"
f"<td>{user_display_name}</td>"
f"<td>{count}</td>"
f"<td>{in_tok_by_user[user_id]}</td>"
f"<td>{out_tok_by_user[user_id]}</td>"
f"<td>{total_tok_by_user[user_id]}</td>"
f"<td>{cost_by_user[user_id]:.4f} ¥</td>"
f"</tr>"
)
else:
html_content_for_tab += "<tr><td colspan='6'>无数据</td></tr>"
html_content_for_tab += "</tbody></table>"
html_content_for_tab += (
"<h2>聊天消息统计</h2><table><thead><tr><th>联系人/群组名称</th><th>消息数量</th></tr></thead><tbody>"
)
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"<tr><td>{chat_name_display}</td><td>{count}</td></tr>"
else:
html_content_for_tab += "<tr><td colspan='2'>无数据</td></tr>"
html_content_for_tab += "</tbody></table></div>"
tab_content_html_list.append(html_content_for_tab)
joined_tab_list = "\n".join(tab_list)
joined_tab_content = "\n".join(tab_content_list)
html_template = (
"""
@ -739,7 +686,6 @@ class StatisticOutputTask(AsyncTask):
border: 1px solid #ddd;
padding: 10px;
text-align: left;
word-break: break-all;
}
th {
background-color: #3498db;
@ -758,38 +704,24 @@ class StatisticOutputTask(AsyncTask):
.tabs {
overflow: hidden;
background: #ecf0f1;
display: flex;
flex-wrap: wrap;
margin-bottom: -1px;
display: flex;
}
.tabs button {
background: inherit;
border: 1px solid #ccc;
border-bottom: none;
outline: none;
padding: 14px 16px;
cursor: pointer;
transition: 0.3s;
font-size: 16px;
margin-right: 2px;
border-radius: 4px 4px 0 0;
background: inherit; border: none; outline: none;
padding: 14px 16px; cursor: pointer;
transition: 0.3s; font-size: 16px;
}
.tabs button:hover {
background-color: #d4dbdc;
}
.tabs button.active {
background-color: #fff;
border-color: #ccc;
border-bottom: 1px solid #fff;
position: relative;
z-index: 1;
background-color: #b3bbbd;
}
.tab-content {
display: none;
padding: 20px;
background-color: #fff;
border: 1px solid #ccc;
border-top: none;
}
.tab-content.active {
display: block;
@ -804,14 +736,10 @@ class StatisticOutputTask(AsyncTask):
<p class="info-item"><strong>统计截止时间:</strong> {now.strftime("%Y-%m-%d %H:%M:%S")}</p>
<div class="tabs">
{"".join(tab_list_html)}
{joined_tab_list}
</div>
{"".join(tab_content_html_list)}
<div class="footer">
<p>Generated by MaiBot Statistics Module</p>
</div>
{joined_tab_content}
</div>
"""
+ """
@ -820,35 +748,20 @@ class StatisticOutputTask(AsyncTask):
tab_content = document.getElementsByClassName("tab-content");
tab_links = document.getElementsByClassName("tab-link");
if (tab_content.length > 0 && tab_links.length > 0) {
tab_content[0].classList.add("active");
tab_links[0].classList.add("active");
}
tab_content[0].classList.add("active");
tab_links[0].classList.add("active");
function showTab(evt, tabName) {
for (i = 0; i < tab_content.length; i++) {
tab_content[i].classList.remove("active");
}
for (i = 0; i < tab_links.length; i++) {
tab_links[i].classList.remove("active");
}
const currentTabContent = document.getElementById(tabName);
if (currentTabContent) {
currentTabContent.classList.add("active");
}
if (evt.currentTarget) {
evt.currentTarget.classList.add("active");
}
}
function showTab(evt, tabName) {{
for (i = 0; i < tab_content.length; i++) tab_content[i].classList.remove("active");
for (i = 0; i < tab_links.length; i++) tab_links[i].classList.remove("active");
document.getElementById(tabName).classList.add("active");
evt.currentTarget.classList.add("active");
}}
</script>
</body>
</html>
"""
)
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}")
with open(self.record_file_path, "w", encoding="utf-8") as f:
f.write(html_template)

View File

@ -1,6 +1,3 @@
# TODO: 更多的可配置项
# TODO: 所有模型单独分离,温度可配置
# TODO: 原生多模态支持
import os
import re
from dataclasses import dataclass, field
@ -285,7 +282,6 @@ class BotConfig:
# enable_think_flow: bool = False # 是否启用思考流程
enable_friend_whitelist: bool = True # 是否启用好友白名单
talk_allowed_private = set()
api_polling_max_retries: int = 3 # 神秘小功能
rename_person: bool = (
True # 是否启用改名工具,可以让麦麦对唯一名进行更改,可能可以更拟人地称呼他人,但是也可能导致记忆混淆的问题
)
@ -424,13 +420,13 @@ class BotConfig:
if config.INNER_VERSION in SpecifierSet(">=1.2.4"):
config.personality_core = personality_config.get("personality_core", config.personality_core)
config.personality_sides = personality_config.get("personality_sides", config.personality_sides)
if config.INNER_VERSION in SpecifierSet(">=1.6.1.2"):
if config.INNER_VERSION in SpecifierSet(">=1.7.1"):
config.personality_detail_level = personality_config.get(
"personality_detail_level", config.personality_sides
)
if config.INNER_VERSION in SpecifierSet(">=1.7.0"):
config.expression_style = personality_config.get("expression_style", config.expression_style)
if config.INNER_VERSION in SpecifierSet(">=1.7.0.3"):
if config.INNER_VERSION in SpecifierSet(">=1.7.1"):
config.enable_expression_learner = personality_config.get(
"enable_expression_learner", config.enable_expression_learner
)
@ -477,7 +473,7 @@ class BotConfig:
config.steal_emoji = emoji_config.get("steal_emoji", config.steal_emoji)
def group_nickname(parent: dict):
if config.INNER_VERSION in SpecifierSet(">=1.6.1.4"):
if config.INNER_VERSION in SpecifierSet(">=1.7.1"):
group_nickname_config = parent.get("group_nickname", {})
config.enable_nickname_mapping = group_nickname_config.get(
"enable_nickname_mapping", config.enable_nickname_mapping
@ -521,7 +517,7 @@ class BotConfig:
config.ban_words = chat_config.get("ban_words", config.ban_words)
for r in chat_config.get("ban_msgs_regex", config.ban_msgs_regex):
config.ban_msgs_regex.add(re.compile(r))
if config.INNER_VERSION in SpecifierSet(">=1.6.1.2"):
if config.INNER_VERSION in SpecifierSet(">=1.7.1"):
config.allow_remove_duplicates = chat_config.get(
"allow_remove_duplicates", config.allow_remove_duplicates
)
@ -691,7 +687,7 @@ class BotConfig:
config.consolidate_memory_percentage = memory_config.get(
"consolidate_memory_percentage", config.consolidate_memory_percentage
)
if config.INNER_VERSION in SpecifierSet(">=1.6.1.3"):
if config.INNER_VERSION in SpecifierSet(">=1.7.1"):
config.long_message_auto_truncate = memory_config.get(
"long_message_auto_truncate", config.long_message_auto_truncate
)
@ -760,21 +756,17 @@ 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.6.2.4"):
if config.INNER_VERSION in SpecifierSet(">=1.7.1"):
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.3"):
if config.INNER_VERSION in SpecifierSet(">=1.7.1"):
config.rename_person = experimental_config.get("rename_person", config.rename_person)
if config.INNER_VERSION in SpecifierSet(">=1.7.0.1"):
if config.INNER_VERSION in SpecifierSet(">=1.7.1"):
config.enable_Legacy_HFC = experimental_config.get("enable_Legacy_HFC", config.enable_Legacy_HFC)
def pfc(parent: dict):
if config.INNER_VERSION in SpecifierSet(">=1.6.2.4"):
if config.INNER_VERSION in SpecifierSet(">=1.7.1"):
pfc_config = parent.get("pfc", {})
# 解析 [pfc] 下的直接字段
config.enable_pfc_chatting = pfc_config.get("enable_pfc_chatting", config.enable_pfc_chatting)

View File

@ -1,92 +0,0 @@
# HeartFChatting 逻辑详解
`HeartFChatting` 类是心流系统Heart Flow System中实现**专注聊天**`ChatState.FOCUSED`)功能的核心。顾名思义,其职责乃是在特定聊天流(`stream_id`)中,模拟更为连贯深入之对话。此非凭空臆造,而是依赖一个持续不断的 **思考(Think)-规划(Plan)-执行(Execute)** 循环。当其所系的 `SubHeartflow` 进入 `FOCUSED` 状态时,便会创建并启动 `HeartFChatting` 实例;若状态转为他途(譬如 `CHAT``ABSENT`),则会将其关闭。
## 1. 初始化简述 (`__init__`, `_initialize`)
创生之初,`HeartFChatting` 需注入若干关键之物:`chat_id`(亦即 `stream_id`)、关联的 `SubMind` 实例,以及 `Observation` 实例(用以观察环境)。
其内部核心组件包括:
- `ActionManager`: 管理当前循环可选之策(如:不应、言语、表情)。
- `HeartFCGenerator` (`self.gpt_instance`): 专司生成回复文本之职。
- `ToolUser` (`self.tool_user`): 虽主要用于获取工具定义,然亦备 `SubMind` 调用之需(实际执行由 `SubMind` 操持)。
- `HeartFCSender` (`self.heart_fc_sender`): 负责消息发送诸般事宜,含"正在思考"之态。
- `LLMRequest` (`self.planner_llm`): 配置用于执行"规划"任务的大语言模型。
*初始化过程采取懒加载策略,仅在首次需要访问 `ChatStream` 时(通常在 `start` 方法中)进行。*
## 2. 生命周期 (`start`, `shutdown`)
- **启动 (`start`)**: 外部调用此法,以启 `HeartFChatting` 之流程。内部会安全地启动主循环任务。
- **关闭 (`shutdown`)**: 外部调用此法,以止其运行。会取消主循环任务,清理状态,并释放锁。
## 3. 核心循环 (`_hfc_loop`) 与 循环记录 (`CycleInfo`)
`_hfc_loop``HeartFChatting` 之脉搏,以异步方式不舍昼夜运行(直至 `shutdown` 被调用)。其核心在于周而复始地执行 **思考-规划-执行** 之周期。
每一轮循环,皆会创建一个 `CycleInfo` 对象。此对象犹如史官,详细记载该次循环之点滴:
- **身份标识**: 循环 ID (`cycle_id`)。
- **时间轨迹**: 起止时刻 (`start_time`, `end_time`)。
- **行动细节**: 是否执行动作 (`action_taken`)、动作类型 (`action_type`)、决策理由 (`reasoning`)。
- **耗时考量**: 各阶段计时 (`timers`)。
- **关联信息**: 思考消息 ID (`thinking_id`)、是否重新规划 (`replanned`)、详尽响应信息 (`response_info`含生成文本、表情、锚点、实际发送ID、`SubMind`思考等)。
这些 `CycleInfo` 被存入一个队列 (`_cycle_history`),近者得观。此记录不仅便于调试,更关键的是,它会作为**上下文信息**传递给下一次循环的"思考"阶段,使得 `SubMind` 能鉴往知来,做出更连贯的决策。
*循环间会根据执行情况智能引入延迟,避免空耗资源。*
## 4. 思考-规划-执行周期 (`_think_plan_execute_loop`)
此乃 `HeartFChatting` 最核心的逻辑单元,每一循环皆按序执行以下三步:
### 4.1. 思考 (`_get_submind_thinking`)
* **第一步:观察环境**: 调用 `Observation``observe()` 方法,感知聊天室是否有新动态(如新消息)。
* **第二步:触发子思维**: 调用关联 `SubMind``do_thinking_before_reply()` 方法。
* **关键点**: 会将**上一个循环**的 `CycleInfo` 传入,让 `SubMind` 了解上次行动的决策、理由及是否重新规划,从而实现"承前启后"的思考。
* `SubMind` 在此阶段不仅进行思考,还可能**调用其配置的工具**来收集信息。
* **第三步:获取成果**: `SubMind` 返回两部分重要信息:
1. 当前的内心想法 (`current_mind`)。
2. 通过工具调用收集到的结构化信息 (`structured_info`)。
### 4.2. 规划 (`_planner`)
* **输入**: 接收来自"思考"阶段的 `current_mind``structured_info`,以及"观察"到的最新消息。
* **目标**: 基于当前想法、已知信息、聊天记录、机器人个性以及可用动作,决定**接下来要做什么**。
* **决策方式**:
1. 构建一个精心设计的提示词 (`_build_planner_prompt`)。
2. 获取 `ActionManager` 中定义的当前可用动作(如 `no_reply`, `text_reply`, `emoji_reply`)作为"工具"选项。
3. 调用大语言模型 (`self.planner_llm`)**强制**其选择一个动作"工具"并提供理由。可选动作包括:
* `no_reply`: 不回复(例如,自己刚说过话或对方未回应)。
* `text_reply`: 发送文本回复。
* `emoji_reply`: 仅发送表情。
* 文本回复亦可附带表情(通过 `emoji_query` 参数指定)。
* **动态调整(重新规划)**:
* 在做出初步决策后,会检查自规划开始后是否有新消息 (`_check_new_messages`)。
* 若有新消息,则有一定概率触发**重新规划**。此时会再次调用规划器,但提示词会包含之前决策的信息,要求 LLM 重新考虑。
* **输出**: 返回一个包含最终决策的字典,主要包括:
* `action`: 选定的动作类型。
* `reasoning`: 做出此决策的理由。
* `emoji_query`: (可选) 如果需要发送表情,指定表情的主题。
### 4.3. 执行 (`_handle_action`)
* **输入**: 接收"规划"阶段输出的 `action`、`reasoning` 和 `emoji_query`
* **行动**: 根据 `action` 的类型,分派到不同的处理函数:
* **文本回复 (`_handle_text_reply`)**:
1. 获取锚点消息(当前实现为系统触发的占位符)。
2. 调用 `HeartFCSender``register_thinking` 标记开始思考。
3. 调用 `HeartFCGenerator` (`_replier_work`) 生成回复文本。**注意**: 回复器逻辑 (`_replier_work`) 本身并非独立复杂组件,主要是调用 `HeartFCGenerator` 完成文本生成。
4. 调用 `HeartFCSender` (`_sender`) 发送生成的文本和可能的表情。**注意**: 发送逻辑 (`_sender`, `_send_response_messages`, `_handle_emoji`) 同样委托给 `HeartFCSender` 实例处理,包含模拟打字、实际发送、存储消息等细节。
* **仅表情回复 (`_handle_emoji_reply`)**:
1. 获取锚点消息。
2. 调用 `HeartFCSender` 发送表情。
* **不回复 (`_handle_no_reply`)**:
1. 记录理由。
2. 进入等待状态 (`_wait_for_new_message`)直到检测到新消息或超时目前300秒期间会监听关闭信号。
## 总结
`HeartFChatting` 通过 **观察 -> 思考(含工具)-> 规划 -> 执行** 的闭环,并利用 `CycleInfo` 进行上下文传递,实现了更加智能和连贯的专注聊天行为。其核心在于利用 `SubMind` 进行深度思考和信息收集,再通过 LLM 规划器进行决策,最后由 `HeartFCSender` 可靠地执行消息发送任务。

View File

@ -1,159 +0,0 @@
# HeartFC_chat 工作原理文档
HeartFC_chat 是一个基于心流理论的聊天系统通过模拟人类的思维过程和情感变化来实现自然的对话交互。系统采用Plan-Replier-Sender循环机制实现了智能化的对话决策和生成。
## 核心工作流程
### 1. 消息处理与存储 (HeartFCProcessor)
[代码位置: src/plugins/heartFC_chat/heartflow_processor.py]
消息处理器负责接收和预处理消息,主要完成以下工作:
```mermaid
graph TD
A[接收原始消息] --> B[解析为MessageRecv对象]
B --> C[消息缓冲处理]
C --> D[过滤检查]
D --> E[存储到数据库]
```
核心实现:
- 消息处理入口:`process_message()` [行号: 38-215]
- 消息解析和缓冲:`message_buffer.start_caching_messages()` [行号: 63]
- 过滤检查:`_check_ban_words()`, `_check_ban_regex()` [行号: 196-215]
- 消息存储:`storage.store_message()` [行号: 108]
### 2. 对话管理循环 (HeartFChatting)
[代码位置: src/plugins/heartFC_chat/heartFC_chat.py]
HeartFChatting是系统的核心组件实现了完整的对话管理循环
```mermaid
graph TD
A[Plan阶段] -->|决策是否回复| B[Replier阶段]
B -->|生成回复内容| C[Sender阶段]
C -->|发送消息| D[等待新消息]
D --> A
```
#### Plan阶段 [行号: 282-386]
- 主要函数:`_planner()`
- 功能实现:
* 获取观察信息:`observation.observe()` [行号: 297]
* 思维处理:`sub_mind.do_thinking_before_reply()` [行号: 301]
* LLM决策使用`PLANNER_TOOL_DEFINITION`进行动作规划 [行号: 13-42]
#### Replier阶段 [行号: 388-416]
- 主要函数:`_replier_work()`
- 调用生成器:`gpt_instance.generate_response()` [行号: 394]
- 处理生成结果和错误情况
#### Sender阶段 [行号: 418-450]
- 主要函数:`_sender()`
- 发送实现:
* 创建消息:`_create_thinking_message()` [行号: 452-477]
* 发送回复:`_send_response_messages()` [行号: 479-525]
* 处理表情:`_handle_emoji()` [行号: 527-567]
### 3. 回复生成机制 (HeartFCGenerator)
[代码位置: src/plugins/heartFC_chat/heartFC_generator.py]
回复生成器负责产生高质量的回复内容:
```mermaid
graph TD
A[获取上下文信息] --> B[构建提示词]
B --> C[调用LLM生成]
C --> D[后处理优化]
D --> E[返回回复集]
```
核心实现:
- 生成入口:`generate_response()` [行号: 39-67]
* 情感调节:`arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()` [行号: 47]
* 模型生成:`_generate_response_with_model()` [行号: 69-95]
* 响应处理:`_process_response()` [行号: 97-106]
### 4. 提示词构建系统 (HeartFlowPromptBuilder)
[代码位置: src/plugins/heartFC_chat/heartflow_prompt_builder.py]
提示词构建器支持两种工作模式HeartFC_chat专门使用Focus模式而Normal模式是为normal_chat设计的
#### 专注模式 (Focus Mode) - HeartFC_chat专用
- 实现函数:`_build_prompt_focus()` [行号: 116-141]
- 特点:
* 专注于当前对话状态和思维
* 更强的目标导向性
* 用于HeartFC_chat的Plan-Replier-Sender循环
* 简化的上下文处理,专注于决策
#### 普通模式 (Normal Mode) - Normal_chat专用
- 实现函数:`_build_prompt_normal()` [行号: 143-215]
- 特点:
* 用于normal_chat的常规对话
* 完整的个性化处理
* 关系系统集成
* 知识库检索:`get_prompt_info()` [行号: 217-591]
HeartFC_chat的Focus模式工作流程
```mermaid
graph TD
A[获取结构化信息] --> B[获取当前思维状态]
B --> C[构建专注模式提示词]
C --> D[用于Plan阶段决策]
D --> E[用于Replier阶段生成]
```
## 智能特性
### 1. 对话决策机制
- LLM决策工具定义`PLANNER_TOOL_DEFINITION` [heartFC_chat.py 行号: 13-42]
- 决策执行:`_planner()` [heartFC_chat.py 行号: 282-386]
- 考虑因素:
* 上下文相关性
* 情感状态
* 兴趣程度
* 对话时机
### 2. 状态管理
[代码位置: src/plugins/heartFC_chat/heartFC_chat.py]
- 状态机实现:`HeartFChatting`类 [行号: 44-567]
- 核心功能:
* 初始化:`_initialize()` [行号: 89-112]
* 循环控制:`_run_pf_loop()` [行号: 192-281]
* 状态转换:`_handle_loop_completion()` [行号: 166-190]
### 3. 回复生成策略
[代码位置: src/plugins/heartFC_chat/heartFC_generator.py]
- 温度调节:`current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier` [行号: 48]
- 生成控制:`_generate_response_with_model()` [行号: 69-95]
- 响应处理:`_process_response()` [行号: 97-106]
## 系统配置
### 关键参数
- LLM配置`model_normal` [heartFC_generator.py 行号: 32-37]
- 过滤规则:`_check_ban_words()`, `_check_ban_regex()` [heartflow_processor.py 行号: 196-215]
- 状态控制:`INITIAL_DURATION = 60.0` [heartFC_chat.py 行号: 11]
### 优化建议
1. 调整LLM参数`temperature`和`max_tokens`
2. 优化提示词模板:`init_prompt()` [heartflow_prompt_builder.py 行号: 8-115]
3. 配置状态转换条件
4. 维护过滤规则
## 注意事项
1. 系统稳定性
- 异常处理各主要函数都包含try-except块
- 状态检查:`_processing_lock`确保并发安全
- 循环控制:`_loop_active`和`_loop_task`管理
2. 性能优化
- 缓存使用:`message_buffer`系统
- LLM调用优化批量处理和复用
- 异步处理:使用`asyncio`
3. 质量控制
- 日志记录:使用`get_module_logger()`
- 错误追踪:详细的异常记录
- 响应监控:完整的状态跟踪

View File

@ -1,94 +0,0 @@
- **智能化 MaiState 状态转换**:
- 当前 `MaiState` (整体状态,如 `OFFLINE`, `NORMAL_CHAT` 等) 的转换逻辑 (`MaiStateManager`) 较为简单,主要依赖时间和随机性。
- 未来的计划是让主心流 (`Heartflow`) 负责决策自身的 `MaiState`
- 该决策将综合考虑以下信息:
- 各个子心流 (`SubHeartflow`) 的活动状态和信息摘要。
- 主心流自身的状态和历史信息。
- (可能) 结合预设的日程安排 (Schedule) 信息。
- 目标是让 Mai 的整体状态变化更符合逻辑和上下文。 (计划在 064 实现)
- **参数化与动态调整聊天行为**:
- 将 `NormalChatInstance``HeartFlowChatInstance` 中的关键行为参数(例如:回复概率、思考频率、兴趣度阈值、状态转换条件等)提取出来,使其更易于配置。
- 允许每个 `SubHeartflow` (即每个聊天场景) 拥有其独立的参数配置,实现"千群千面"。
- 开发机制,使得这些参数能够被动态调整:
- 基于外部反馈:例如,根据用户评价("话太多"或"太冷淡")调整回复频率。
- 基于环境分析:例如,根据群消息的活跃度自动调整参与度。
- 基于学习:通过分析历史交互数据,优化特定群聊下的行为模式。
- 目标是让 Mai 在不同群聊中展现出更适应环境、更个性化的交互风格。
- **动态 Prompt 生成与人格塑造**:
- 当前 Prompt (提示词) 相对静态。计划实现动态或半结构化的 Prompt 生成。
- Prompt 内容可根据以下因素调整:
- **人格特质**: 通过参数化配置(如友善度、严谨性等),影响 Prompt 的措辞、语气和思考倾向,塑造更稳定和独特的人格。
- **当前情绪**: 将实时情绪状态融入 Prompt使回复更符合当下心境。
- 目标:提升 `HeartFlowChatInstance` (HFC) 回复的多样性、一致性和真实感。
- 前置:需要重构 Prompt 构建逻辑,可能引入 `PromptBuilder` 并提供标准接口 (认为是必须步骤)。
- **扩展观察系统 (Observation System)**:
- 目前主要依赖 `ChattingObservation` 获取消息。
- 计划引入更多 `Observation` 类型,为 `SubHeartflow` 提供更丰富的上下文:
- Mai 的全局状态 (`MaiStateInfo`)。
- `SubHeartflow` 自身的聊天状态 (`ChatStateInfo`) 和参数配置。
- Mai 的系统配置、连接平台信息。
- 其他相关聊天或系统的聚合信息。
- 目标:让 `SubHeartflow` 基于更全面的信息进行决策。
- **增强工具调用能力 (Enhanced Tool Usage)**:
- 扩展 `HeartFlowChatInstance` (HFC) 可用的工具集。
- 考虑引入"元工具"或分层工具机制,允许 HFC 在需要时(如深度思考)访问更强大的工具,例如:
- 修改自身或其他 `SubHeartflow` 的聊天参数。
- 请求改变 Mai 的全局状态 (`MaiState`)。
- 管理日程或执行更复杂的分析任务。
- 目标:提升 HFC 的自主决策和行动能力,即使会增加一定的延迟。
- **基于历史学习的行为模式应用**:
- **学习**: 分析过往聊天记录,提取和学习具体的行为模式(如特定梗的用法、情境化回应风格等)。可能需要专门的分析模块。
- **存储与匹配**: 需要有效的方法存储学习到的行为模式,并开发强大的 **匹配** 机制,在运行时根据当前情境检索最合适的模式。**(匹配的准确性是关键)**
- **应用与评估**: 将匹配到的行为模式融入 HFC 的决策和回复生成(例如,将其整合进 Prompt。之后需评估该行为模式应用的实际效果。
- **人格塑造**: 通过学习到的实际行为来动态塑造人格,作为静态人设描述的补充或替代,使其更生动自然。
- **标准化人设生成 (Standardized Persona Generation)**:
- **目标**: 解决手动配置 `人设` 文件缺乏标准、难以全面描述个性的问题,并生成更丰富、可操作的人格资源。
- **方法**: 利用大型语言模型 (LLM) 辅助生成标准化的、结构化的人格**资源包**。
- **生成内容**: 不仅生成描述性文本(替代现有 `individual` 配置),还可以同时生成与该人格配套的:
- **相关工具 (Tools)**: 该人格倾向于使用的工具或能力。
- **初始记忆/知识库 (Memories/Knowledge)**: 定义其背景和知识基础。
- **核心行为模式 (Core Behavior Patterns)**: 预置一些典型的行为方式,可作为行为学习的起点。
- **实现途径**:
- 通过与 LLM 的交互式对话来定义和细化人格及其配套资源。
- 让 LLM 分析提供的文本材料(如小说、背景故事)来提取人格特质和相关信息。
- **优势**: 替代易出错且标准不一的手动配置,生成更丰富、一致、包含配套资源且易于系统理解和应用的人格包。
- **优化表情包处理与理解 (Enhanced Emoji Handling and Understanding)**:
- **面临挑战**:
- **历史记录表示**: 如何在聊天历史中有效表示表情包,供 LLM 理解。
- **语义理解**: 如何让 LLM 准确把握表情包的含义、情感和语境。
- **场景判断与选择**: 如何让 LLM 判断何时适合使用表情包,并选择最贴切的一个。
- **目标**: 提升 Mai 理解和运用表情包的能力,使交互更自然生动。
- **说明**: 可能需要较多时间进行数据处理和模型调优,但对改善体验潜力巨大。
- **探索高级记忆检索机制 (GE 系统概念):**
- 研究超越简单关键词/近期性检索的记忆模型。
- 考虑引入基于事件关联、相对时间线索和绝对时间锚点的检索方式。
- 可能涉及设计新的事件表示或记忆结构。
- **实现 SubHeartflow 级记忆缓存池:**
- 在 `SubHeartflow` 层级或更高层级设计并实现一个缓存池,存储已检索的记忆/信息。
- 避免在 HFC 等循环中重复进行相同的记忆检索调用。
- 确保存储的信息能有效服务于当前交互上下文。
- **基于人格生成预设知识:**
- 开发利用 LLM 和人格配置生成背景知识的功能。
- 这些知识应符合角色的行为风格和可能的经历。
- 作为一种"冷启动"或丰富角色深度的方式。
## 开发计划TODOLIST
- 人格功能WIP
- 对特定对象的侧写功能
- 图片发送转发功能WIP
- 幽默和meme功能WIP
- 小程序转发链接解析
- 自动生成的回复逻辑,例如自生成的回复方向,回复风格

View File

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

View File

@ -1,5 +1,5 @@
[inner]
version = "1.7.0.3"
version = "1.7.1"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请在修改后将version的值进行变更
@ -206,7 +206,6 @@ enable_Legacy_HFC = false # 是否启用旧 HFC 处理器
enable_friend_chat = true # 是否启用好友聊天
enable_friend_whitelist = true # 是否启用好友聊天白名单
talk_allowed_private = [] # 可以回复消息的QQ号
api_polling_max_retries = 3 # 神秘小功能
rename_person = true # 是否启用改名工具,可以让麦麦对唯一名进行更改,可能可以更拟人地称呼他人,但是也可能导致记忆混淆的问题
[pfc]

View File

@ -15,28 +15,16 @@ DATABASE_NAME=MegBot
# MONGODB_PASSWORD=password
# MONGODB_AUTH_SOURCE=admin
# 配置代理信息
PROXY_HOST=127.0.0.1
PROXY_PORT=7890
# 如果 PROXY_MODELS 包含特殊字符或空格,最好用引号括起来,使用英文逗号分割
PROXY_MODELS="gemini-2.0-flash"
#key and url
CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1/
CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1/
DEEP_SEEK_BASE_URL=https://api.deepseek.com/v1/
GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta/models
DEEP_SEEK_BASE_URL=https://api.deepseek.com/v1
# 定义你要用的api的key(需要去对应网站申请哦)
DEEP_SEEK_KEY=
CHAT_ANY_WHERE_KEY=
SILICONFLOW_KEY=
GEMINI_KEY='[
"KEY1",
"KEY2",
"KEY3"
]'
abandon_GEMINI_KEY='[]'
# 定义日志相关配置
# 精简控制台输出格式