Merge branch 'dev' of https://github.com/Dax233/MaiMBot into PFC-test

pull/937/head
Bakadax 2025-05-06 16:33:36 +08:00
commit 9cb1841fb6
34 changed files with 815 additions and 1070 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ mongodb/
NapCat.Framework.Windows.Once/
log/
logs/
out/
tool_call_benchmark.py
run_maibot_core.bat
run_napcat_adapter.bat

36
bot.py
View File

@ -17,6 +17,11 @@ from rich.traceback import install
install(extra_lines=3)
# 设置工作目录为脚本所在目录
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
print(f"已设置工作目录为: {script_dir}")
logger = get_logger("main")
confirm_logger = get_logger("confirm")
@ -226,6 +231,7 @@ def raw_main():
if __name__ == "__main__":
exit_code = 0 # 用于记录程序最终的退出状态
try:
# 获取MainSystem实例
main_system = raw_main()
@ -241,13 +247,29 @@ if __name__ == "__main__":
except KeyboardInterrupt:
# loop.run_until_complete(global_api.stop())
logger.warning("收到中断信号,正在优雅关闭...")
loop.run_until_complete(graceful_shutdown())
finally:
loop.close()
if loop and not loop.is_closed():
try:
loop.run_until_complete(graceful_shutdown())
except Exception as ge: # 捕捉优雅关闭时可能发生的错误
logger.error(f"优雅关闭时发生错误: {ge}")
# except Exception as e: # 将主异常捕获移到外层 try...except
# logger.error(f"事件循环内发生错误: {str(e)} {str(traceback.format_exc())}")
# exit_code = 1
# finally: # finally 块移到最外层,确保 loop 关闭和暂停总是执行
# if loop and not loop.is_closed():
# loop.close()
# # 在这里添加 input() 来暂停
# input("按 Enter 键退出...") # <--- 添加这行
# sys.exit(exit_code) # <--- 使用记录的退出码
except Exception as e:
logger.error(f"主程序异常: {str(e)} {str(traceback.format_exc())}")
if loop and not loop.is_closed():
loop.run_until_complete(graceful_shutdown())
logger.error(f"主程序发生异常: {str(e)} {str(traceback.format_exc())}")
exit_code = 1 # 标记发生错误
finally:
# 确保 loop 在任何情况下都尝试关闭(如果存在且未关闭)
if "loop" in locals() and loop and not loop.is_closed():
loop.close()
sys.exit(1)
logger.info("事件循环已关闭")
# 在程序退出前暂停,让你有机会看到输出
input("按 Enter 键退出...") # <--- 添加这行
sys.exit(exit_code) # <--- 使用记录的退出码

Binary file not shown.

View File

@ -85,6 +85,7 @@ def handle_import_openie(openie_data: OpenIE, embed_manager: EmbeddingManager, k
logger.error("系统将于2秒后开始检查数据完整性")
sleep(2)
found_missing = False
missing_idxs = []
for doc in getattr(openie_data, "docs", []):
idx = doc.get("idx", "<无idx>")
passage = doc.get("passage", "<无passage>")
@ -104,14 +105,38 @@ def handle_import_openie(openie_data: OpenIE, embed_manager: EmbeddingManager, k
# print(f"检查: idx={idx}")
if missing:
found_missing = True
missing_idxs.append(idx)
logger.error("\n")
logger.error("数据缺失:")
logger.error(f"对应哈希值:{idx}")
logger.error(f"对应文段内容内容:{passage}")
logger.error(f"非法原因:{', '.join(missing)}")
# 确保提示在所有非法数据输出后再输出
if not found_missing:
print("所有数据均完整,没有发现缺失字段。")
return False
logger.info("所有数据均完整,没有发现缺失字段。")
return False
# 新增:提示用户是否删除非法文段继续导入
# 将print移到所有logger.error之后确保不会被冲掉
logger.info("\n检测到非法文段,共{}条。".format(len(missing_idxs)))
logger.info("\n是否删除所有非法文段后继续导入?(y/n): ", end="")
user_choice = input().strip().lower()
if user_choice != "y":
logger.info("用户选择不删除非法文段,程序终止。")
sys.exit(1)
# 删除非法文段
logger.info("正在删除非法文段并继续导入...")
# 过滤掉非法文段
openie_data.docs = [
doc for doc in getattr(openie_data, "docs", []) if doc.get("idx", "<无idx>") not in missing_idxs
]
# 重新提取数据
raw_paragraphs = openie_data.extract_raw_paragraph_dict()
entity_list_data = openie_data.extract_entity_dict()
triple_list_data = openie_data.extract_triple_dict()
# 再次校验
if len(raw_paragraphs) != len(entity_list_data) or len(raw_paragraphs) != len(triple_list_data):
logger.error("删除非法文段后,数据仍不一致,程序终止。")
sys.exit(1)
# 将索引换为对应段落的hash值
logger.info("正在进行段落去重与重索引")
raw_paragraphs, triple_list_data = hash_deduplicate(
@ -174,7 +199,13 @@ def main():
embed_manager.load_from_file()
except Exception as e:
logger.error("从文件加载Embedding库时发生错误{}".format(e))
logger.error("如果你是第一次导入知识,请忽略此错误")
if "嵌入模型与本地存储不一致" in str(e):
logger.error("检测到嵌入模型与本地存储不一致,已终止导入。请检查模型设置或清空嵌入库后重试。")
logger.error("请保证你的嵌入模型从未更改,并且在导入时使用相同的模型")
# print("检测到嵌入模型与本地存储不一致,已终止导入。请检查模型设置或清空嵌入库后重试。")
sys.exit(1)
if "不存在" in str(e):
logger.error("如果你是第一次导入知识,请忽略此错误")
logger.info("Embedding库加载完成")
# 初始化KG
kg_manager = KGManager()

View File

@ -1,670 +0,0 @@
import tkinter as tk
from tkinter import ttk
import time
import os
from datetime import datetime, timedelta
import random
from collections import deque
import json # 引入 json
# --- 引入 Matplotlib ---
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.dates as mdates # 用于处理日期格式
import matplotlib # 导入 matplotlib
# --- 配置 ---
LOG_FILE_PATH = os.path.join("logs", "interest", "interest_history.log") # 指向历史日志文件
REFRESH_INTERVAL_MS = 200 # 刷新间隔 (毫秒) - 可以适当调长,因为读取文件可能耗时
WINDOW_TITLE = "Interest Monitor (Live History)"
MAX_HISTORY_POINTS = 1000 # 图表上显示的最大历史点数 (可以增加)
MAX_STREAMS_TO_DISPLAY = 15 # 最多显示多少个聊天流的折线图 (可以增加)
MAX_QUEUE_SIZE = 30 # 新增:历史想法队列最大长度
# *** 添加 Matplotlib 中文字体配置 ***
# 尝试使用 'SimHei' 或 'Microsoft YaHei'如果找不到matplotlib 会回退到默认字体
# 确保你的系统上安装了这些字体
matplotlib.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"]
matplotlib.rcParams["axes.unicode_minus"] = False # 解决负号'-'显示为方块的问题
def get_random_color():
"""生成随机颜色用于区分线条"""
return "#{:06x}".format(random.randint(0, 0xFFFFFF))
def format_timestamp(ts):
"""辅助函数:格式化时间戳,处理 None 或无效值"""
if ts is None:
return "N/A"
try:
# 假设 ts 是 float 类型的时间戳
dt_object = datetime.fromtimestamp(float(ts))
return dt_object.strftime("%Y-%m-%d %H:%M:%S")
except (ValueError, TypeError):
return "Invalid Time"
class InterestMonitorApp:
def __init__(self, root):
self._main_mind_loaded = None
self.root = root
self.root.title(WINDOW_TITLE)
self.root.geometry("1800x800") # 调整窗口大小以适应图表
# --- 数据存储 ---
# 使用 deque 来存储有限的历史数据点
# key: stream_id, value: deque([(timestamp, interest_level), ...])
self.stream_history = {}
# key: stream_id, value: deque([(timestamp, reply_probability), ...])
self.probability_history = {}
self.stream_colors = {} # 为每个 stream 分配颜色
self.stream_display_names = {} # 存储显示名称 (group_name)
self.selected_stream_id = tk.StringVar() # 用于 Combobox 绑定
# --- 新增:存储其他参数 ---
# 顶层信息
self.latest_main_mind = tk.StringVar(value="N/A")
self.latest_mai_state = tk.StringVar(value="N/A")
self.latest_subflow_count = tk.IntVar(value=0)
# 子流最新状态 (key: stream_id)
self.stream_sub_minds = {}
self.stream_chat_states = {}
self.stream_threshold_status = {}
self.stream_last_active = {}
self.stream_last_interaction = {}
# 用于显示单个流详情的 StringVar
self.single_stream_sub_mind = tk.StringVar(value="想法: N/A")
self.single_stream_chat_state = tk.StringVar(value="状态: N/A")
self.single_stream_threshold = tk.StringVar(value="阈值: N/A")
self.single_stream_last_active = tk.StringVar(value="活跃: N/A")
self.single_stream_last_interaction = tk.StringVar(value="交互: N/A")
# 新增:历史想法队列
self.main_mind_history = deque(maxlen=MAX_QUEUE_SIZE)
self.last_main_mind_timestamp = 0 # 记录最后一条main_mind的时间戳
# --- UI 元素 ---
# --- 新增:顶部全局信息框架 ---
self.global_info_frame = ttk.Frame(root, padding="5 0 5 5") # 顶部内边距调整
self.global_info_frame.pack(side=tk.TOP, fill=tk.X, pady=(5, 0)) # 底部外边距为0
ttk.Label(self.global_info_frame, text="全局状态:").pack(side=tk.LEFT, padx=(0, 10))
ttk.Label(self.global_info_frame, textvariable=self.latest_mai_state).pack(side=tk.LEFT, padx=5)
ttk.Label(self.global_info_frame, text="想法:").pack(side=tk.LEFT, padx=(10, 0))
ttk.Label(self.global_info_frame, textvariable=self.latest_main_mind).pack(side=tk.LEFT, padx=5)
ttk.Label(self.global_info_frame, text="子流数:").pack(side=tk.LEFT, padx=(10, 0))
ttk.Label(self.global_info_frame, textvariable=self.latest_subflow_count).pack(side=tk.LEFT, padx=5)
# 创建 Notebook (选项卡控件)
self.notebook = ttk.Notebook(root)
# 修改fill 和 expand让 notebook 填充剩余空间
self.notebook.pack(pady=(5, 0), padx=10, fill=tk.BOTH, expand=1) # 顶部外边距改小
# --- 第一个选项卡:所有流 ---
self.frame_all = ttk.Frame(self.notebook, padding="5 5 5 5")
self.notebook.add(self.frame_all, text="所有聊天流")
# 状态标签 (移动到最底部)
self.status_label = tk.Label(root, text="Initializing...", anchor="w", fg="grey")
self.status_label.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=(0, 5)) # 调整边距
# Matplotlib 图表设置 (用于第一个选项卡)
self.fig = Figure(figsize=(5, 4), dpi=100)
self.ax = self.fig.add_subplot(111)
# 配置在 update_plot 中进行,避免重复
# 创建 Tkinter 画布嵌入 Matplotlib 图表 (用于第一个选项卡)
self.canvas = FigureCanvasTkAgg(self.fig, master=self.frame_all) # <--- 放入 frame_all
self.canvas_widget = self.canvas.get_tk_widget()
self.canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# --- 第二个选项卡:单个流 ---
self.frame_single = ttk.Frame(self.notebook, padding="5 5 5 5")
self.notebook.add(self.frame_single, text="单个聊天流详情")
# 单个流选项卡的上部控制区域
self.control_frame_single = ttk.Frame(self.frame_single)
self.control_frame_single.pack(side=tk.TOP, fill=tk.X, pady=5)
ttk.Label(self.control_frame_single, text="选择聊天流:").pack(side=tk.LEFT, padx=(0, 5))
self.stream_selector = ttk.Combobox(
self.control_frame_single, textvariable=self.selected_stream_id, state="readonly", width=50
)
self.stream_selector.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.stream_selector.bind("<<ComboboxSelected>>", self.on_stream_selected)
# --- 新增:单个流详情显示区域 ---
self.single_stream_details_frame = ttk.Frame(self.frame_single, padding="5 5 5 0")
self.single_stream_details_frame.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
ttk.Label(self.single_stream_details_frame, textvariable=self.single_stream_sub_mind).pack(side=tk.LEFT, padx=5)
ttk.Label(self.single_stream_details_frame, textvariable=self.single_stream_chat_state).pack(
side=tk.LEFT, padx=5
)
ttk.Label(self.single_stream_details_frame, textvariable=self.single_stream_threshold).pack(
side=tk.LEFT, padx=5
)
ttk.Label(self.single_stream_details_frame, textvariable=self.single_stream_last_active).pack(
side=tk.LEFT, padx=5
)
ttk.Label(self.single_stream_details_frame, textvariable=self.single_stream_last_interaction).pack(
side=tk.LEFT, padx=5
)
# Matplotlib 图表设置 (用于第二个选项卡)
self.fig_single = Figure(figsize=(5, 4), dpi=100)
# 修改:创建两个子图,一个显示兴趣度,一个显示概率
self.ax_single_interest = self.fig_single.add_subplot(211) # 2行1列的第1个
self.ax_single_probability = self.fig_single.add_subplot(
212, sharex=self.ax_single_interest
) # 2行1列的第2个共享X轴
# 创建 Tkinter 画布嵌入 Matplotlib 图表 (用于第二个选项卡)
self.canvas_single = FigureCanvasTkAgg(self.fig_single, master=self.frame_single) # <--- 放入 frame_single
self.canvas_widget_single = self.canvas_single.get_tk_widget()
self.canvas_widget_single.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# --- 新增第三个选项卡:麦麦历史想法 ---
self.frame_mind_history = ttk.Frame(self.notebook, padding="5 5 5 5")
self.notebook.add(self.frame_mind_history, text="麦麦历史想法")
# 聊天框样式的文本框(只读)+ 滚动条
self.mind_text_scroll = tk.Scrollbar(self.frame_mind_history)
self.mind_text_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.mind_text = tk.Text(
self.frame_mind_history,
height=25,
state="disabled",
wrap="word",
font=("微软雅黑", 12),
yscrollcommand=self.mind_text_scroll.set,
)
self.mind_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=1, padx=5, pady=5)
self.mind_text_scroll.config(command=self.mind_text.yview)
# --- 初始化和启动刷新 ---
self.update_display() # 首次加载并开始刷新循环
def on_stream_selected(self, event=None):
"""当 Combobox 选择改变时调用,更新单个流的图表"""
self.update_single_stream_plot()
def load_main_mind_history(self):
"""只读取包含main_mind的日志行维护历史想法队列"""
if not os.path.exists(LOG_FILE_PATH):
return
main_mind_entries = []
try:
with open(LOG_FILE_PATH, "r", encoding="utf-8") as f:
for line in f:
try:
log_entry = json.loads(line.strip())
if "main_mind" in log_entry:
ts = log_entry.get("timestamp", 0)
main_mind_entries.append((ts, log_entry))
except Exception:
continue
main_mind_entries.sort(key=lambda x: x[0])
recent_entries = main_mind_entries[-MAX_QUEUE_SIZE:]
self.main_mind_history.clear()
for _ts, entry in recent_entries:
self.main_mind_history.append(entry)
if recent_entries:
self.last_main_mind_timestamp = recent_entries[-1][0]
# 首次加载时刷新
self.refresh_mind_text()
except Exception:
pass
def update_main_mind_history(self):
"""实时监控log文件发现新main_mind数据则更新队列和展示仅有新数据时刷新"""
if not os.path.exists(LOG_FILE_PATH):
return
new_entries = []
try:
with open(LOG_FILE_PATH, "r", encoding="utf-8") as f:
for line in reversed(list(f)):
try:
log_entry = json.loads(line.strip())
if "main_mind" in log_entry:
ts = log_entry.get("timestamp", 0)
if ts > self.last_main_mind_timestamp:
new_entries.append((ts, log_entry))
else:
break
except Exception:
continue
if new_entries:
for ts, entry in sorted(new_entries):
if len(self.main_mind_history) >= MAX_QUEUE_SIZE:
self.main_mind_history.popleft()
self.main_mind_history.append(entry)
self.last_main_mind_timestamp = ts
self.refresh_mind_text() # 只有有新数据时才刷新
except Exception:
pass
def refresh_mind_text(self):
"""刷新聊天框样式的历史想法展示"""
self.mind_text.config(state="normal")
self.mind_text.delete(1.0, tk.END)
for entry in self.main_mind_history:
ts = entry.get("timestamp", 0)
dt_str = datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if ts else ""
main_mind = entry.get("main_mind", "")
mai_state = entry.get("mai_state", "")
subflow_count = entry.get("subflow_count", "")
msg = f"[{dt_str}] 状态:{mai_state} 子流:{subflow_count}\n{main_mind}\n\n"
self.mind_text.insert(tk.END, msg)
self.mind_text.see(tk.END)
self.mind_text.config(state="disabled")
def load_and_update_history(self):
"""从 history log 文件加载数据并更新历史记录"""
if not os.path.exists(LOG_FILE_PATH):
self.set_status(f"Error: Log file not found at {LOG_FILE_PATH}", "red")
# 如果文件不存在,不清空现有数据,以便显示最后一次成功读取的状态
return
# *** Reset display names each time we reload ***
new_stream_history = {}
new_stream_display_names = {}
new_probability_history = {} # <--- 重置概率历史
# --- 新增:重置其他子流状态 --- (如果需要的话,但通常覆盖即可)
# self.stream_sub_minds = {}
# self.stream_chat_states = {}
# ... 等等 ...
read_count = 0
error_count = 0
# *** Calculate the timestamp threshold for the last 30 minutes ***
current_time = time.time()
time_threshold = current_time - (15 * 60) # 30 minutes in seconds
try:
with open(LOG_FILE_PATH, "r", encoding="utf-8") as f:
for line in f:
read_count += 1
try:
log_entry = json.loads(line.strip())
timestamp = log_entry.get("timestamp") # 获取顶层时间戳
# *** 时间过滤 ***
if timestamp is None:
error_count += 1
continue # 跳过没有时间戳的行
try:
entry_timestamp = float(timestamp)
if entry_timestamp < time_threshold:
continue # 跳过时间过早的条目
except (ValueError, TypeError):
error_count += 1
continue # 跳过时间戳格式错误的行
# --- 新增:更新顶层信息 (使用最后一个有效行的数据) ---
self.latest_main_mind.set(
log_entry.get("main_mind", self.latest_main_mind.get())
) # 保留旧值如果缺失
self.latest_mai_state.set(log_entry.get("mai_state", self.latest_mai_state.get()))
self.latest_subflow_count.set(log_entry.get("subflow_count", self.latest_subflow_count.get()))
# --- 修改开始:迭代 subflows ---
subflows = log_entry.get("subflows")
if not isinstance(subflows, list): # 检查 subflows 是否存在且为列表
error_count += 1
continue # 跳过没有 subflows 或格式无效的行
for subflow_entry in subflows:
stream_id = subflow_entry.get("stream_id")
interest_level = subflow_entry.get("interest_level")
# 获取 group_name如果不存在则回退到 stream_id
group_name = subflow_entry.get("group_name", stream_id)
# reply_probability = subflow_entry.get("reply_probability") # 获取概率值 # <-- 注释掉旧行
start_hfc_probability = subflow_entry.get(
"start_hfc_probability"
) # <-- 添加新行,读取新字段
# *** 检查必要的字段 ***
# 注意:时间戳已在顶层检查过
if stream_id is None or interest_level is None:
# 这里可以选择记录子流错误,但暂时跳过
continue # 跳过无效的 subflow 条目
# 确保 interest_level 可以转换为浮点数
try:
interest_level_float = float(interest_level)
except (ValueError, TypeError):
continue # 跳过 interest_level 无效的 subflow
# 如果是第一次读到这个 stream_id则创建 deque
if stream_id not in new_stream_history:
new_stream_history[stream_id] = deque(maxlen=MAX_HISTORY_POINTS)
new_probability_history[stream_id] = deque(maxlen=MAX_HISTORY_POINTS) # 创建概率 deque
# 检查是否已有颜色,没有则分配
if stream_id not in self.stream_colors:
self.stream_colors[stream_id] = get_random_color()
# *** 存储此 stream_id 最新的显示名称 ***
new_stream_display_names[stream_id] = group_name
# --- 新增:存储其他子流信息 ---
self.stream_sub_minds[stream_id] = subflow_entry.get("sub_mind", "N/A")
self.stream_chat_states[stream_id] = subflow_entry.get("sub_chat_state", "N/A")
self.stream_threshold_status[stream_id] = subflow_entry.get("is_above_threshold", False)
self.stream_last_active[stream_id] = subflow_entry.get(
"chat_state_changed_time"
) # 存储原始时间戳
# 添加数据点 (使用顶层时间戳)
new_stream_history[stream_id].append((entry_timestamp, interest_level_float))
# 添加概率数据点 (如果存在且有效)
# if reply_probability is not None: # <-- 注释掉旧判断
if start_hfc_probability is not None: # <-- 修改判断条件
try:
# 尝试将概率转换为浮点数
# probability_float = float(reply_probability) # <-- 注释掉旧转换
probability_float = float(start_hfc_probability) # <-- 使用新变量
new_probability_history[stream_id].append((entry_timestamp, probability_float))
except (TypeError, ValueError):
# 如果概率值无效,可以跳过或记录一个默认值,这里跳过
pass
# --- 修改结束 ---
except json.JSONDecodeError:
error_count += 1
# logger.warning(f"Skipping invalid JSON line: {line.strip()}")
continue # 跳过无法解析的行
# except (TypeError, ValueError) as e: # 这个外层 catch 可能不再需要,因为类型错误在内部处理了
# error_count += 1
# # logger.warning(f"Skipping line due to data type error ({e}): {line.strip()}")
# continue # 跳过数据类型错误的行
# 读取完成后,用新数据替换旧数据
self.stream_history = new_stream_history
self.stream_display_names = new_stream_display_names # *** Update display names ***
self.probability_history = new_probability_history # <--- 更新概率历史
# 清理不再存在的 stream_id 的附加信息 (可选,但保持一致性)
streams_to_remove = set(self.stream_sub_minds.keys()) - set(new_stream_history.keys())
for sid in streams_to_remove:
self.stream_sub_minds.pop(sid, None)
self.stream_chat_states.pop(sid, None)
self.stream_threshold_status.pop(sid, None)
self.stream_last_active.pop(sid, None)
self.stream_last_interaction.pop(sid, None)
# 颜色和显示名称也应该清理,但当前逻辑是保留旧颜色
# self.stream_colors.pop(sid, None)
status_msg = f"Data loaded at {datetime.now().strftime('%H:%M:%S')}. Lines read: {read_count}."
if error_count > 0:
status_msg += f" Skipped {error_count} invalid lines."
self.set_status(status_msg, "orange")
else:
self.set_status(status_msg, "green")
except IOError as e:
self.set_status(f"Error reading file {LOG_FILE_PATH}: {e}", "red")
except Exception as e:
self.set_status(f"An unexpected error occurred during loading: {e}", "red")
# --- 更新 Combobox ---
self.update_stream_selector()
def update_stream_selector(self):
"""更新单个流选项卡中的 Combobox 列表"""
# 创建 (display_name, stream_id) 对的列表,按 display_name 排序
available_streams = sorted(
[
(name, sid)
for sid, name in self.stream_display_names.items()
if sid in self.stream_history and self.stream_history[sid]
],
key=lambda item: item[0], # 按显示名称排序
)
# 更新 Combobox 的值 (仅显示 display_name)
self.stream_selector["values"] = [name for name, sid in available_streams]
# 检查当前选中的 stream_id 是否仍然有效
current_selection_name = self.selected_stream_id.get()
current_selection_valid = any(name == current_selection_name for name, sid in available_streams)
if not current_selection_valid and available_streams:
# 如果当前选择无效,并且有可选流,则默认选中第一个
self.selected_stream_id.set(available_streams[0][0])
# 手动触发一次更新,因为 set 不会触发 <<ComboboxSelected>>
self.update_single_stream_plot()
elif not available_streams:
# 如果没有可选流,清空选择
self.selected_stream_id.set("")
self.update_single_stream_plot() # 清空图表
def update_all_streams_plot(self):
"""更新第一个选项卡的 Matplotlib 图表 (显示所有流)"""
self.ax.clear() # 清除旧图
# *** 设置中文标题和标签 ***
self.ax.set_title("兴趣度随时间变化图 (所有活跃流)")
self.ax.set_xlabel("时间")
self.ax.set_ylabel("兴趣度")
self.ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M:%S"))
self.ax.grid(True)
self.ax.set_ylim(0, 10) # 固定 Y 轴范围 0-10
# 只绘制最新的 N 个 stream (按最后记录的兴趣度排序)
# 注意:现在是基于文件读取的快照排序,可能不是实时最新
active_streams = sorted(
self.stream_history.items(),
key=lambda item: item[1][-1][1] if item[1] else 0, # 按最后兴趣度排序
reverse=True,
)[:MAX_STREAMS_TO_DISPLAY]
all_times = [] # 用于确定 X 轴范围
for stream_id, history in active_streams:
if not history:
continue
timestamps, interests = zip(*history)
# 将 time.time() 时间戳转换为 matplotlib 可识别的日期格式
try:
mpl_dates = [datetime.fromtimestamp(ts) for ts in timestamps]
all_times.extend(mpl_dates) # 收集所有时间点
# *** Use display name for label ***
display_label = self.stream_display_names.get(stream_id, stream_id)
self.ax.plot(
mpl_dates,
interests,
label=display_label, # *** Use display_label ***
color=self.stream_colors.get(stream_id, "grey"),
marker=".",
markersize=3,
linestyle="-",
linewidth=1,
)
except ValueError as e:
print(f"Skipping plot for {stream_id} due to invalid timestamp: {e}")
continue
if all_times:
# 根据数据动态调整 X 轴范围,留一点边距
min_time = min(all_times)
max_time = max(all_times)
# delta = max_time - min_time
# self.ax.set_xlim(min_time - delta * 0.05, max_time + delta * 0.05)
self.ax.set_xlim(min_time, max_time)
# 自动格式化X轴标签
self.fig.autofmt_xdate()
else:
# 如果没有数据,设置一个默认的时间范围,例如最近一小时
now = datetime.now()
one_hour_ago = now - timedelta(hours=1)
self.ax.set_xlim(one_hour_ago, now)
# 添加图例
if active_streams:
# 调整图例位置和大小
# 字体已通过全局 matplotlib.rcParams 设置
self.ax.legend(loc="upper left", bbox_to_anchor=(1.02, 1), borderaxespad=0.0, fontsize="x-small")
# 调整布局,确保图例不被裁剪
self.fig.tight_layout(rect=[0, 0, 0.85, 1]) # 右侧留出空间给图例
self.canvas.draw() # 重绘画布
def update_single_stream_plot(self):
"""更新第二个选项卡的 Matplotlib 图表 (显示单个选定的流)"""
self.ax_single_interest.clear()
self.ax_single_probability.clear()
# 设置子图标题和标签
self.ax_single_interest.set_title("兴趣度")
self.ax_single_interest.set_ylim(0, 10) # 固定 Y 轴范围 0-10
# self.ax_single_probability.set_title("回复评估概率") # <-- 注释掉旧标题
self.ax_single_probability.set_title("HFC 启动概率") # <-- 修改标题
self.ax_single_probability.set_xlabel("时间")
# self.ax_single_probability.set_ylabel("概率") # <-- 注释掉旧标签
self.ax_single_probability.set_ylabel("HFC 概率") # <-- 修改 Y 轴标签
self.ax_single_probability.grid(True)
self.ax_single_probability.set_ylim(0, 1.05) # 固定 Y 轴范围 0-1
self.ax_single_probability.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M:%S"))
selected_name = self.selected_stream_id.get()
selected_sid = None
# --- 新增:根据选中的名称找到 stream_id ---
if selected_name:
for sid, name in self.stream_display_names.items():
if name == selected_name:
selected_sid = sid
break
all_times = [] # 用于确定 X 轴范围
# --- 新增:绘制兴趣度图 ---
if selected_sid and selected_sid in self.stream_history and self.stream_history[selected_sid]:
history = self.stream_history[selected_sid]
timestamps, interests = zip(*history)
try:
mpl_dates = [datetime.fromtimestamp(ts) for ts in timestamps]
all_times.extend(mpl_dates)
self.ax_single_interest.plot(
mpl_dates,
interests,
color=self.stream_colors.get(selected_sid, "blue"),
marker=".",
markersize=3,
linestyle="-",
linewidth=1,
)
except ValueError as e:
print(f"Skipping interest plot for {selected_sid} due to invalid timestamp: {e}")
# --- 新增:绘制概率图 ---
if selected_sid and selected_sid in self.probability_history and self.probability_history[selected_sid]:
prob_history = self.probability_history[selected_sid]
prob_timestamps, probabilities = zip(*prob_history)
try:
prob_mpl_dates = [datetime.fromtimestamp(ts) for ts in prob_timestamps]
# 注意:概率图的时间点可能与兴趣度不同,也需要加入 all_times
all_times.extend(prob_mpl_dates)
self.ax_single_probability.plot(
prob_mpl_dates,
probabilities,
color=self.stream_colors.get(selected_sid, "green"), # 可以用不同颜色
marker=".",
markersize=3,
linestyle="-",
linewidth=1,
)
except ValueError as e:
print(f"Skipping probability plot for {selected_sid} due to invalid timestamp: {e}")
# --- 新增:调整 X 轴范围和格式 ---
if all_times:
min_time = min(all_times)
max_time = max(all_times)
# 设置共享的 X 轴范围
self.ax_single_interest.set_xlim(min_time, max_time)
# self.ax_single_probability.set_xlim(min_time, max_time) # sharex 会自动同步
# 自动格式化X轴标签 (应用到共享轴的最后一个子图上通常即可)
self.fig_single.autofmt_xdate()
else:
# 如果没有数据,设置一个默认的时间范围
now = datetime.now()
one_hour_ago = now - timedelta(hours=1)
self.ax_single_interest.set_xlim(one_hour_ago, now)
# self.ax_single_probability.set_xlim(one_hour_ago, now) # sharex 会自动同步
# --- 新增:更新单个流的详细信息标签 ---
self.update_single_stream_details(selected_sid)
# --- 新增:重新绘制画布 ---
self.canvas_single.draw()
def update_single_stream_details(self, stream_id):
"""更新单个流详情区域的标签内容"""
if stream_id:
sub_mind = self.stream_sub_minds.get(stream_id, "N/A")
chat_state = self.stream_chat_states.get(stream_id, "N/A")
threshold = self.stream_threshold_status.get(stream_id, False)
last_active_ts = self.stream_last_active.get(stream_id)
last_interaction_ts = self.stream_last_interaction.get(stream_id)
self.single_stream_sub_mind.set(f"想法: {sub_mind}")
self.single_stream_chat_state.set(f"状态: {chat_state}")
self.single_stream_threshold.set(f"阈值以上: {'' if threshold else ''}")
self.single_stream_last_active.set(f"最后活跃: {format_timestamp(last_active_ts)}")
self.single_stream_last_interaction.set(f"最后交互: {format_timestamp(last_interaction_ts)}")
else:
# 如果没有选择流,则清空详情
self.single_stream_sub_mind.set("想法: N/A")
self.single_stream_chat_state.set("状态: N/A")
self.single_stream_threshold.set("阈值: N/A")
self.single_stream_last_active.set("活跃: N/A")
self.single_stream_last_interaction.set("交互: N/A")
def update_display(self):
"""主更新循环"""
try:
# --- 新增:首次加载历史想法 ---
if not hasattr(self, "_main_mind_loaded"):
self.load_main_mind_history()
self._main_mind_loaded = True
else:
self.update_main_mind_history() # 只有有新main_mind数据时才刷新界面
# *** 修改:分别调用两个图表的更新方法 ***
self.load_and_update_history() # 从文件加载数据并更新内部状态
self.update_all_streams_plot() # 更新所有流的图表
self.update_single_stream_plot() # 更新单个流的图表
except Exception as e:
# 提供更详细的错误信息
import traceback
error_msg = f"Error during update: {e}\n{traceback.format_exc()}"
self.set_status(error_msg, "red")
print(error_msg) # 打印详细错误到控制台
# 安排下一次刷新
self.root.after(REFRESH_INTERVAL_MS, self.update_display)
def set_status(self, message: str, color: str = "grey"):
"""更新状态栏标签"""
# 限制状态栏消息长度
max_len = 150
display_message = (message[:max_len] + "...") if len(message) > max_len else message
self.status_label.config(text=display_message, fg=color)
if __name__ == "__main__":
# 导入 timedelta 用于默认时间范围
from datetime import timedelta
root = tk.Tk()
app = InterestMonitorApp(root)
root.mainloop()

View File

@ -5,12 +5,21 @@ import sys # 新增系统模块导入
import datetime # 新增导入
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from src.common.logger import get_module_logger
from src.common.logger_manager import get_logger
from src.plugins.knowledge.src.lpmmconfig import global_config
logger = get_module_logger("LPMM数据库-原始数据处理")
logger = get_logger("lpmm")
ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
RAW_DATA_PATH = os.path.join(ROOT_PATH, "data/lpmm_raw_data")
IMPORTED_DATA_PATH = os.path.join(ROOT_PATH, "data/imported_lpmm_data")
# 新增:确保 RAW_DATA_PATH 存在
if not os.path.exists(RAW_DATA_PATH):
os.makedirs(RAW_DATA_PATH, exist_ok=True)
logger.info(f"已创建目录: {RAW_DATA_PATH}")
if global_config.get("persistence", {}).get("raw_data_path") is not None:
IMPORTED_DATA_PATH = os.path.join(ROOT_PATH, global_config["persistence"]["raw_data_path"])
else:
IMPORTED_DATA_PATH = os.path.join(ROOT_PATH, "data/imported_lpmm_data")
# 添加项目根目录到 sys.path
@ -54,7 +63,7 @@ def main():
print("请确保原始数据已放置在正确的目录中。")
confirm = input("确认继续执行?(y/n): ").strip().lower()
if confirm != "y":
logger.error("操作已取消")
logger.info("操作已取消")
sys.exit(1)
print("\n" + "=" * 40 + "\n")
@ -94,6 +103,6 @@ def main():
if __name__ == "__main__":
print(f"Raw Data Path: {RAW_DATA_PATH}")
print(f"Imported Data Path: {IMPORTED_DATA_PATH}")
logger.info(f"原始数据路径: {RAW_DATA_PATH}")
logger.info(f"处理后的数据路径: {IMPORTED_DATA_PATH}")
main()

View File

@ -1,8 +0,0 @@
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
app = FastAPI()
graphql_router = GraphQLRouter(schema=None, path="/") # Replace `None` with your actual schema
app.include_router(graphql_router, prefix="/graphql", tags=["GraphQL"])

View File

@ -0,0 +1,16 @@
from src.heart_flow.heartflow import heartflow
from src.heart_flow.sub_heartflow import ChatState
async def get_all_subheartflow_ids() -> list:
"""获取所有子心流的ID列表"""
all_subheartflows = heartflow.subheartflow_manager.get_all_subheartflows()
return [subheartflow.subheartflow_id for subheartflow in all_subheartflows]
async def forced_change_subheartflow_status(subheartflow_id: str, status: ChatState) -> bool:
"""强制改变子心流的状态"""
subheartflow = await heartflow.get_or_create_subheartflow(subheartflow_id)
if subheartflow:
return await heartflow.force_change_subheartflow_status(subheartflow_id, status)
return False

View File

@ -1,155 +1,187 @@
from typing import List, Optional
from typing import List, Optional, Dict, Any
import strawberry
# from packaging.version import Version, InvalidVersion
# from packaging.specifiers import SpecifierSet, InvalidSpecifier
# from ..config.config import global_config
# import os
from packaging.version import Version
import os
ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
@strawberry.type
class BotConfig:
class APIBotConfig:
"""机器人配置类"""
INNER_VERSION: Version
INNER_VERSION: Version # 配置文件内部版本号
MAI_VERSION: str # 硬编码的版本信息
# bot
BOT_QQ: Optional[int]
BOT_NICKNAME: Optional[str]
BOT_ALIAS_NAMES: List[str] # 别名,可以通过这个叫它
BOT_QQ: Optional[int] # 机器人QQ号
BOT_NICKNAME: Optional[str] # 机器人昵称
BOT_ALIAS_NAMES: List[str] # 机器人别名列表
# group
talk_allowed_groups: set
talk_frequency_down_groups: set
ban_user_id: set
talk_allowed_groups: List[int] # 允许回复消息的群号列表
talk_frequency_down_groups: List[int] # 降低回复频率的群号列表
ban_user_id: List[int] # 禁止回复和读取消息的QQ号列表
# personality
personality_core: str # 建议20字以内谁再写3000字小作文敲谁脑袋
personality_sides: List[str]
personality_core: str # 人格核心特点描述
personality_sides: List[str] # 人格细节描述列表
# identity
identity_detail: List[str]
height: int # 身高 单位厘米
weight: int # 体重 单位千克
age: int # 年龄 单位岁
identity_detail: List[str] # 身份特点列表
age: int # 年龄(岁)
gender: str # 性别
appearance: str # 外貌特征
appearance: str # 外貌特征描述
# schedule
ENABLE_SCHEDULE_GEN: bool # 是否启用日程生成
PROMPT_SCHEDULE_GEN: str
SCHEDULE_DOING_UPDATE_INTERVAL: int # 日程表更新间隔 单位秒
SCHEDULE_TEMPERATURE: float # 日程表温度建议0.5-1.0
ENABLE_SCHEDULE_INTERACTION: bool # 是否启用日程交互
PROMPT_SCHEDULE_GEN: str # 日程生成提示词
SCHEDULE_DOING_UPDATE_INTERVAL: int # 日程进行中更新间隔
SCHEDULE_TEMPERATURE: float # 日程生成温度
TIME_ZONE: str # 时区
# message
MAX_CONTEXT_SIZE: int # 上下文最大消息数
emoji_chance: float # 发送表情包的基础概率
thinking_timeout: int # 思考时间
model_max_output_length: int # 最大回复长度
message_buffer: bool # 消息缓冲器
# platforms
platforms: Dict[str, str] # 平台信息
ban_words: set
ban_msgs_regex: set
# heartflow
# enable_heartflow: bool = False # 是否启用心流
sub_heart_flow_update_interval: int # 子心流更新频率,间隔 单位秒
sub_heart_flow_freeze_time: int # 子心流冻结时间,超过这个时间没有回复,子心流会冻结,间隔 单位秒
sub_heart_flow_stop_time: int # 子心流停止时间,超过这个时间没有回复,子心流会停止,间隔 单位秒
heart_flow_update_interval: int # 心流更新频率,间隔 单位秒
observation_context_size: int # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩
compressed_length: int # 不能大于observation_context_size,心流上下文压缩的最短压缩长度超过心流观察到的上下文长度会压缩最短压缩长度为5
compress_length_limit: int # 最多压缩份数,超过该数值的压缩上下文会被删除
# chat
allow_focus_mode: bool # 是否允许专注模式
base_normal_chat_num: int # 基础普通聊天次数
base_focused_chat_num: int # 基础专注聊天次数
observation_context_size: int # 观察上下文大小
message_buffer: bool # 是否启用消息缓冲
ban_words: List[str] # 禁止词列表
ban_msgs_regex: List[str] # 禁止消息的正则表达式列表
# willing
# normal_chat
MODEL_R1_PROBABILITY: float # 模型推理概率
MODEL_V3_PROBABILITY: float # 模型普通概率
emoji_chance: float # 表情符号出现概率
thinking_timeout: int # 思考超时时间
willing_mode: str # 意愿模式
response_willing_amplifier: float # 回复意愿放大系数
response_interested_rate_amplifier: float # 回复兴趣度放大系数
down_frequency_rate: float # 降低回复频率的群组回复意愿降低系数
emoji_response_penalty: float # 表情回复惩罚
mentioned_bot_inevitable_reply: bool # 提及 bot 必然回复
at_bot_inevitable_reply: bool # @bot 必然回复
response_willing_amplifier: float # 回复意愿放大
response_interested_rate_amplifier: float # 回复兴趣率放大器
down_frequency_rate: float # 降低频率率
emoji_response_penalty: float # 表情回复惩罚
mentioned_bot_inevitable_reply: bool # 提到机器人时是否必定回复
at_bot_inevitable_reply: bool # @机器人时是否必定回复
# response
response_mode: str # 回复策略
MODEL_R1_PROBABILITY: float # R1模型概率
MODEL_V3_PROBABILITY: float # V3模型概率
# MODEL_R1_DISTILL_PROBABILITY: float # R1蒸馏模型概率
# focus_chat
reply_trigger_threshold: float # 回复触发阈值
default_decay_rate_per_second: float # 默认每秒衰减率
consecutive_no_reply_threshold: int # 连续不回复阈值
# compressed
compressed_length: int # 压缩长度
compress_length_limit: int # 压缩长度限制
# emoji
max_emoji_num: int # 表情包最大数量
max_reach_deletion: bool # 开启则在达到最大数量时删除表情包,关闭则不会继续收集表情包
EMOJI_CHECK_INTERVAL: int # 表情检查间隔(分钟)
EMOJI_REGISTER_INTERVAL: int # 表情包注册间隔(分钟
EMOJI_SAVE: bool # 偷表情包
EMOJI_CHECK: bool # 是否开启过滤
EMOJI_CHECK_PROMPT: str # 表情包过滤要求
max_emoji_num: int # 最大表情符号数量
max_reach_deletion: bool # 达到最大数量时是否删除
EMOJI_CHECK_INTERVAL: int # 表情检查间隔
EMOJI_REGISTER_INTERVAL: Optional[int] # 表情注册间隔(兼容性保留
EMOJI_SAVE: bool # 是否保存表情
EMOJI_CHECK: bool # 是否检查表情
EMOJI_CHECK_PROMPT: str # 表情检查提示词
# memory
build_memory_interval: int # 记忆构建间隔(秒)
memory_build_distribution: list # 记忆构建分布参数分布1均值标准差权重分布2均值标准差权重
build_memory_sample_num: int # 记忆构建采样数量
build_memory_sample_length: int # 记忆构建采样长度
build_memory_interval: int # 构建记忆间隔
memory_build_distribution: List[float] # 记忆构建分布
build_memory_sample_num: int # 构建记忆样本数量
build_memory_sample_length: int # 构建记忆样本长度
memory_compress_rate: float # 记忆压缩率
forget_memory_interval: int # 记忆遗忘间隔(秒)
memory_forget_time: int # 记忆遗忘时间(小时)
memory_forget_percentage: float # 记忆遗忘比例
memory_ban_words: list # 添加新的配置项默认值
forget_memory_interval: int # 忘记记忆间隔
memory_forget_time: int # 记忆忘记时间
memory_forget_percentage: float # 记忆忘记百分比
consolidate_memory_interval: int # 巩固记忆间隔
consolidation_similarity_threshold: float # 巩固相似度阈值
consolidation_check_percentage: float # 巩固检查百分比
memory_ban_words: List[str] # 记忆禁止词列表
# mood
mood_update_interval: float # 情绪更新间隔 单位秒
mood_update_interval: float # 情绪更新间隔
mood_decay_rate: float # 情绪衰减率
mood_intensity_factor: float # 情绪强度因子
# keywords
keywords_reaction_rules: list # 关键词回复规则
# keywords_reaction
keywords_reaction_enable: bool # 是否启用关键词反应
keywords_reaction_rules: List[Dict[str, Any]] # 关键词反应规则
# chinese_typo
chinese_typo_enable: bool # 是否启用中文错别字生成器
chinese_typo_error_rate: float # 单字替换概
chinese_typo_min_freq: int # 最小字频阈值
chinese_typo_tone_error_rate: float # 声调错误
chinese_typo_word_replace_rate: float # 整词替换概
chinese_typo_enable: bool # 是否启用中文错别字
chinese_typo_error_rate: float # 中文错别字错误
chinese_typo_min_freq: int # 中文错别字最小频率
chinese_typo_tone_error_rate: float # 中文错别字声调错误率
chinese_typo_word_replace_rate: float # 中文错别字单词替换
# response_splitter
enable_response_splitter: bool # 是否启用回复分割器
response_max_length: int # 回复允许的最大长度
response_max_sentence_num: int # 回复允许的最大句子数
response_max_length: int # 回复最大长度
response_max_sentence_num: int # 回复最大句子数
enable_kaomoji_protection: bool # 是否启用颜文字保护
model_max_output_length: int # 模型最大输出长度
# remote
remote_enable: bool # 是否启用远程控制
remote_enable: bool # 是否启用远程功能
# experimental
enable_friend_chat: bool # 是否启用好友聊天
# enable_think_flow: bool # 是否启用思考流程
talk_allowed_private: List[int] # 允许私聊的QQ号列表
enable_pfc_chatting: bool # 是否启用PFC聊天
# 模型配置
llm_reasoning: dict[str, str] # LLM推理
# llm_reasoning_minor: dict[str, str]
llm_normal: dict[str, str] # LLM普通
llm_topic_judge: dict[str, str] # LLM话题判断
llm_summary: dict[str, str] # LLM话题总结
llm_emotion_judge: dict[str, str] # LLM情感判断
embedding: dict[str, str] # 嵌入
vlm: dict[str, str] # VLM
moderation: dict[str, str] # 审核
llm_reasoning: Dict[str, Any] # 推理模型配置
llm_normal: Dict[str, Any] # 普通模型配置
llm_topic_judge: Dict[str, Any] # 主题判断模型配置
llm_summary: Dict[str, Any] # 总结模型配置
llm_emotion_judge: Optional[Dict[str, Any]] # 情绪判断模型配置(兼容性保留)
embedding: Dict[str, Any] # 嵌入模型配置
vlm: Dict[str, Any] # VLM模型配置
moderation: Optional[Dict[str, Any]] # 审核模型配置(兼容性保留)
llm_observation: Dict[str, Any] # 观察模型配置
llm_sub_heartflow: Dict[str, Any] # 子心流模型配置
llm_heartflow: Dict[str, Any] # 心流模型配置
llm_plan: Optional[Dict[str, Any]] # 计划模型配置
llm_PFC_action_planner: Optional[Dict[str, Any]] # PFC行动计划模型配置
llm_PFC_chat: Optional[Dict[str, Any]] # PFC聊天模型配置
llm_PFC_reply_checker: Optional[Dict[str, Any]] # PFC回复检查模型配置
llm_tool_use: Optional[Dict[str, Any]] # 工具使用模型配置
# 实验性
llm_observation: dict[str, str] # LLM观察
llm_sub_heartflow: dict[str, str] # LLM子心流
llm_heartflow: dict[str, str] # LLM心流
api_urls: dict[str, str] # API URLs
api_urls: Optional[Dict[str, str]] # API地址配置
@strawberry.type
class EnvConfig:
pass
class APIEnvConfig:
"""环境变量配置"""
HOST: str # 服务主机地址
PORT: int # 服务端口
PLUGINS: List[str] # 插件列表
MONGODB_HOST: str # MongoDB 主机地址
MONGODB_PORT: int # MongoDB 端口
DATABASE_NAME: str # 数据库名称
CHAT_ANY_WHERE_BASE_URL: str # ChatAnywhere 基础URL
SILICONFLOW_BASE_URL: str # SiliconFlow 基础URL
DEEP_SEEK_BASE_URL: str # DeepSeek 基础URL
DEEP_SEEK_KEY: Optional[str] # DeepSeek API Key
CHAT_ANY_WHERE_KEY: Optional[str] # ChatAnywhere API Key
SILICONFLOW_KEY: Optional[str] # SiliconFlow API Key
SIMPLE_OUTPUT: Optional[bool] # 是否简化输出
CONSOLE_LOG_LEVEL: Optional[str] # 控制台日志等级
FILE_LOG_LEVEL: Optional[str] # 文件日志等级
DEFAULT_CONSOLE_LOG_LEVEL: Optional[str] # 默认控制台日志等级
DEFAULT_FILE_LOG_LEVEL: Optional[str] # 默认文件日志等级
@strawberry.field
def get_env(self) -> str:
return "env"
print("当前路径:")
print(ROOT_PATH)

56
src/api/main.py 100644
View File

@ -0,0 +1,56 @@
from fastapi import APIRouter
from strawberry.fastapi import GraphQLRouter
# from src.config.config import BotConfig
from src.common.logger_manager import get_logger
from src.api.reload_config import reload_config as reload_config_func
from src.common.server import global_server
from .apiforgui import get_all_subheartflow_ids, forced_change_subheartflow_status
from src.heart_flow.sub_heartflow import ChatState
# import uvicorn
# import os
router = APIRouter()
logger = get_logger("api")
# maiapi = FastAPI()
logger.info("麦麦API服务器已启动")
graphql_router = GraphQLRouter(schema=None, path="/") # Replace `None` with your actual schema
router.include_router(graphql_router, prefix="/graphql", tags=["GraphQL"])
@router.post("/config/reload")
async def reload_config():
return await reload_config_func()
@router.get("/gui/subheartflow/get/all")
async def get_subheartflow_ids():
"""获取所有子心流的ID列表"""
return await get_all_subheartflow_ids()
@router.post("/gui/subheartflow/forced_change_status")
async def forced_change_subheartflow_status_api(subheartflow_id: str, status: ChatState): # noqa
"""强制改变子心流的状态"""
# 参数检查
if not isinstance(status, ChatState):
logger.warning(f"无效的状态参数: {status}")
return {"status": "failed", "reason": "invalid status"}
logger.info(f"尝试将子心流 {subheartflow_id} 状态更改为 {status.value}")
success = await forced_change_subheartflow_status(subheartflow_id, status)
if success:
logger.info(f"子心流 {subheartflow_id} 状态更改为 {status.value} 成功")
return {"status": "success"}
else:
logger.error(f"子心流 {subheartflow_id} 状态更改为 {status.value} 失败")
return {"status": "failed"}
def start_api_server():
"""启动API服务器"""
global_server.register_router(router, prefix="/api/v1")

View File

@ -0,0 +1,24 @@
from fastapi import HTTPException
from rich.traceback import install
from src.config.config import BotConfig
from src.common.logger_manager import get_logger
import os
install(extra_lines=3)
logger = get_logger("api")
async def reload_config():
try:
from src.config import config as config_module
logger.debug("正在重载配置文件...")
bot_config_path = os.path.join(BotConfig.get_config_dir(), "bot_config.toml")
config_module.global_config = BotConfig.load_config(config_path=bot_config_path)
logger.debug("配置文件重载成功")
return {"status": "reloaded"}
except FileNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e)) from e
except Exception as e:
raise HTTPException(status_code=500, detail=f"重载配置时发生错误: {str(e)}") from e

View File

@ -358,6 +358,23 @@ SUB_HEARTFLOW_STYLE_CONFIG = {
},
}
INTEREST_CHAT_STYLE_CONFIG = {
"advanced": {
"console_format": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-blue>兴趣</light-blue> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 兴趣 | {message}",
},
"simple": {
"console_format": "<level>{time:MM-DD HH:mm}</level> | <fg #55DDFF>兴趣 | {message}</fg #55DDFF>", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 兴趣 | {message}",
},
}
SUB_HEARTFLOW_MIND_STYLE_CONFIG = {
"advanced": {
"console_format": (
@ -808,6 +825,22 @@ INIT_STYLE_CONFIG = {
},
}
API_SERVER_STYLE_CONFIG = {
"advanced": {
"console_format": (
"<white>{time:YYYY-MM-DD HH:mm:ss}</white> | "
"<level>{level: <8}</level> | "
"<light-yellow>API服务</light-yellow> | "
"<level>{message}</level>"
),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | API服务 | {message}",
},
"simple": {
"console_format": "<level>{time:MM-DD HH:mm}</level> | <light-green>API服务</light-green> | {message}",
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | API服务 | {message}",
},
}
# 根据SIMPLE_OUTPUT选择配置
MAIN_STYLE_CONFIG = MAIN_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MAIN_STYLE_CONFIG["advanced"]
@ -878,6 +911,10 @@ CHAT_MESSAGE_STYLE_CONFIG = (
)
CHAT_IMAGE_STYLE_CONFIG = CHAT_IMAGE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CHAT_IMAGE_STYLE_CONFIG["advanced"]
INIT_STYLE_CONFIG = INIT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else INIT_STYLE_CONFIG["advanced"]
API_SERVER_STYLE_CONFIG = API_SERVER_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else API_SERVER_STYLE_CONFIG["advanced"]
INTEREST_CHAT_STYLE_CONFIG = (
INTEREST_CHAT_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else INTEREST_CHAT_STYLE_CONFIG["advanced"]
)
def is_registered_module(record: dict) -> bool:

View File

@ -41,6 +41,8 @@ from src.common.logger import (
CHAT_MESSAGE_STYLE_CONFIG,
CHAT_IMAGE_STYLE_CONFIG,
INIT_STYLE_CONFIG,
INTEREST_CHAT_STYLE_CONFIG,
API_SERVER_STYLE_CONFIG,
)
# 可根据实际需要补充更多模块配置
@ -86,6 +88,8 @@ MODULE_LOGGER_CONFIGS = {
"chat_message": CHAT_MESSAGE_STYLE_CONFIG, # 聊天消息
"chat_image": CHAT_IMAGE_STYLE_CONFIG, # 聊天图片
"init": INIT_STYLE_CONFIG, # 初始化
"interest_chat": INTEREST_CHAT_STYLE_CONFIG, # 兴趣
"api": API_SERVER_STYLE_CONFIG, # API服务器
# ...如有更多模块,继续添加...
}

View File

@ -8,14 +8,16 @@ logger = get_logger("rename_person_tool")
class RenamePersonTool(BaseTool):
name = "rename_person"
description = "这个工具可以改变用户的昵称。你可以选择改变对他人的称呼。"
description = (
"这个工具可以改变用户的昵称。你可以选择改变对他人的称呼。你想给人改名,叫别人别的称呼,需要调用这个工具。"
)
parameters = {
"type": "object",
"properties": {
"person_name": {"type": "string", "description": "需要重新取名的用户的当前昵称"},
"message_content": {
"type": "string",
"description": "可选的。当前的聊天内容或特定要求,用于提供取名建议的上下文。",
"description": "当前的聊天内容或特定要求,用于提供取名建议的上下文,尽可能详细",
},
},
"required": ["person_name"],

View File

@ -78,6 +78,7 @@ class BackgroundTaskManager:
self._into_focus_task: Optional[asyncio.Task] = None
self._private_chat_activation_task: Optional[asyncio.Task] = None # 新增私聊激活任务引用
self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks
self._detect_command_from_gui_task: Optional[asyncio.Task] = None # 新增GUI命令检测任务引用
async def start_tasks(self):
"""启动所有后台任务
@ -135,6 +136,13 @@ class BackgroundTaskManager:
f"私聊激活检查任务已启动 间隔:{PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS}s",
"_private_chat_activation_task",
),
# 新增GUI命令检测任务配置
# (
# lambda: self._run_detect_command_from_gui_cycle(3),
# "debug",
# f"GUI命令检测任务已启动 间隔:{3}s",
# "_detect_command_from_gui_task",
# ),
]
# 统一启动所有任务
@ -296,3 +304,11 @@ class BackgroundTaskManager:
interval=interval,
task_func=self.subheartflow_manager.sbhf_absent_private_into_focus,
)
# # 有api之后删除
# async def _run_detect_command_from_gui_cycle(self, interval: int):
# await _run_periodic_loop(
# task_name="Detect Command from GUI",
# interval=interval,
# task_func=self.subheartflow_manager.detect_command_from_gui,
# )

View File

@ -1,4 +1,4 @@
from src.heart_flow.sub_heartflow import SubHeartflow
from src.heart_flow.sub_heartflow import SubHeartflow, ChatState
from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
from src.plugins.schedule.schedule_generator import bot_schedule
@ -62,6 +62,11 @@ class Heartflow:
# 不再需要传入 self.current_state
return await self.subheartflow_manager.get_or_create_subheartflow(subheartflow_id)
async def force_change_subheartflow_status(self, subheartflow_id: str, status: ChatState) -> None:
"""强制改变子心流的状态"""
# 这里的 message 是可选的,可能是一个消息对象,也可能是其他类型的数据
return await self.subheartflow_manager.force_change_state(subheartflow_id, status)
async def heartflow_start_working(self):
"""启动后台任务"""
await self.background_task_manager.start_tasks()

View File

@ -0,0 +1,200 @@
import asyncio
from src.config.config import global_config
from typing import Optional, Dict
import traceback
from src.common.logger_manager import get_logger
from src.plugins.chat.message import MessageRecv
import math
# 定义常量 (从 interest.py 移动过来)
MAX_INTEREST = 15.0
logger = get_logger("interest_chatting")
PROBABILITY_INCREASE_RATE_PER_SECOND = 0.1
PROBABILITY_DECREASE_RATE_PER_SECOND = 0.1
MAX_REPLY_PROBABILITY = 1
class InterestChatting:
def __init__(
self,
decay_rate=global_config.default_decay_rate_per_second,
max_interest=MAX_INTEREST,
trigger_threshold=global_config.reply_trigger_threshold,
max_probability=MAX_REPLY_PROBABILITY,
):
# 基础属性初始化
self.interest_level: float = 0.0
self.decay_rate_per_second: float = decay_rate
self.max_interest: float = max_interest
self.trigger_threshold: float = trigger_threshold
self.max_reply_probability: float = max_probability
self.is_above_threshold: bool = False
# 任务相关属性初始化
self.update_task: Optional[asyncio.Task] = None
self._stop_event = asyncio.Event()
self._task_lock = asyncio.Lock()
self._is_running = False
self.interest_dict: Dict[str, tuple[MessageRecv, float, bool]] = {}
self.update_interval = 1.0
self.above_threshold = False
self.start_hfc_probability = 0.0
async def initialize(self):
async with self._task_lock:
if self._is_running:
logger.debug("后台兴趣更新任务已在运行中。")
return
# 清理已完成或已取消的任务
if self.update_task and (self.update_task.done() or self.update_task.cancelled()):
self.update_task = None
if not self.update_task:
self._stop_event.clear()
self._is_running = True
self.update_task = asyncio.create_task(self._run_update_loop(self.update_interval))
logger.debug("后台兴趣更新任务已创建并启动。")
def add_interest_dict(self, message: MessageRecv, interest_value: float, is_mentioned: bool):
"""添加消息到兴趣字典
参数:
message: 接收到的消息
interest_value: 兴趣值
is_mentioned: 是否被提及
功能:
1. 将消息添加到兴趣字典
2. 更新最后交互时间
3. 如果字典长度超过10删除最旧的消息
"""
# 添加新消息
self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned)
# 如果字典长度超过10删除最旧的消息
if len(self.interest_dict) > 10:
oldest_key = next(iter(self.interest_dict))
self.interest_dict.pop(oldest_key)
async def _calculate_decay(self):
"""计算兴趣值的衰减
参数:
current_time: 当前时间戳
处理逻辑:
1. 计算时间差
2. 处理各种异常情况(负值/零值)
3. 正常计算衰减
4. 更新最后更新时间
"""
# 处理极小兴趣值情况
if self.interest_level < 1e-9:
self.interest_level = 0.0
return
# 异常情况处理
if self.decay_rate_per_second <= 0:
logger.warning(f"衰减率({self.decay_rate_per_second})无效重置兴趣值为0")
self.interest_level = 0.0
return
# 正常衰减计算
try:
decay_factor = math.pow(self.decay_rate_per_second, self.update_interval)
self.interest_level *= decay_factor
except ValueError as e:
logger.error(
f"衰减计算错误: {e} 参数: 衰减率={self.decay_rate_per_second} 时间差={self.update_interval} 当前兴趣={self.interest_level}"
)
self.interest_level = 0.0
async def _update_reply_probability(self):
self.above_threshold = self.interest_level >= self.trigger_threshold
if self.above_threshold:
self.start_hfc_probability += PROBABILITY_INCREASE_RATE_PER_SECOND
else:
if self.start_hfc_probability > 0:
self.start_hfc_probability = max(0, self.start_hfc_probability - PROBABILITY_DECREASE_RATE_PER_SECOND)
async def increase_interest(self, value: float):
self.interest_level += value
self.interest_level = min(self.interest_level, self.max_interest)
async def decrease_interest(self, value: float):
self.interest_level -= value
self.interest_level = max(self.interest_level, 0.0)
async def get_interest(self) -> float:
return self.interest_level
async def get_state(self) -> dict:
interest = self.interest_level # 直接使用属性值
return {
"interest_level": round(interest, 2),
"start_hfc_probability": round(self.start_hfc_probability, 4),
"above_threshold": self.above_threshold,
}
# --- 新增后台更新任务相关方法 ---
async def _run_update_loop(self, update_interval: float = 1.0):
"""后台循环,定期更新兴趣和回复概率。"""
try:
while not self._stop_event.is_set():
try:
if self.interest_level != 0:
await self._calculate_decay()
await self._update_reply_probability()
# 等待下一个周期或停止事件
await asyncio.wait_for(self._stop_event.wait(), timeout=update_interval)
except asyncio.TimeoutError:
# 正常超时,继续循环
continue
except Exception as e:
logger.error(f"InterestChatting 更新循环出错: {e}")
logger.error(traceback.format_exc())
# 防止错误导致CPU飙升稍作等待
await asyncio.sleep(5)
except asyncio.CancelledError:
logger.info("InterestChatting 更新循环被取消。")
finally:
self._is_running = False
logger.info("InterestChatting 更新循环已停止。")
async def stop_updates(self):
"""停止后台更新任务,使用锁确保并发安全"""
async with self._task_lock:
if not self._is_running:
logger.debug("后台兴趣更新任务未运行。")
return
logger.info("正在停止 InterestChatting 后台更新任务...")
self._stop_event.set()
if self.update_task and not self.update_task.done():
try:
# 等待任务结束,设置超时
await asyncio.wait_for(self.update_task, timeout=5.0)
logger.info("InterestChatting 后台更新任务已成功停止。")
except asyncio.TimeoutError:
logger.warning("停止 InterestChatting 后台任务超时,尝试取消...")
self.update_task.cancel()
try:
await self.update_task # 等待取消完成
except asyncio.CancelledError:
logger.info("InterestChatting 后台更新任务已被取消。")
except Exception as e:
logger.error(f"停止 InterestChatting 后台任务时发生异常: {e}")
finally:
self.update_task = None
self._is_running = False

View File

@ -29,6 +29,14 @@ def _ensure_log_directory():
logger.info(f"已确保日志目录 '{LOG_DIRECTORY}' 存在")
def _clear_and_create_log_file():
"""清除日志文件并创建新的日志文件。"""
if os.path.exists(os.path.join(LOG_DIRECTORY, HISTORY_LOG_FILENAME)):
os.remove(os.path.join(LOG_DIRECTORY, HISTORY_LOG_FILENAME))
with open(os.path.join(LOG_DIRECTORY, HISTORY_LOG_FILENAME), "w", encoding="utf-8") as f:
f.write("")
class InterestLogger:
"""负责定期记录主心流和所有子心流的状态到日志文件。"""
@ -44,6 +52,7 @@ class InterestLogger:
self.heartflow = heartflow # 存储 Heartflow 实例
self._history_log_file_path = os.path.join(LOG_DIRECTORY, HISTORY_LOG_FILENAME)
_ensure_log_directory()
_clear_and_create_log_file()
async def get_all_subflow_states(self) -> Dict[str, Dict]:
"""并发获取所有活跃子心流的当前完整状态。"""
@ -92,7 +101,7 @@ class InterestLogger:
try:
current_timestamp = time.time()
main_mind = self.heartflow.current_mind
# main_mind = self.heartflow.current_mind
# 获取 Mai 状态名称
mai_state_name = self.heartflow.current_state.get_current_state().name
@ -100,7 +109,7 @@ class InterestLogger:
log_entry_base = {
"timestamp": round(current_timestamp, 2),
"main_mind": main_mind,
# "main_mind": main_mind,
"mai_state": mai_state_name,
"subflow_count": len(all_subflow_states),
"subflows": [],
@ -135,7 +144,7 @@ class InterestLogger:
"sub_chat_state": state.get("chat_state", "未知"),
"interest_level": interest_state.get("interest_level", 0.0),
"start_hfc_probability": interest_state.get("start_hfc_probability", 0.0),
"is_above_threshold": interest_state.get("is_above_threshold", False),
# "is_above_threshold": interest_state.get("is_above_threshold", False),
}
subflow_details.append(subflow_entry)

View File

@ -1,215 +1,22 @@
from .observation import Observation, ChattingObservation
import asyncio
from src.config.config import global_config
import time
from typing import Optional, List, Dict, Tuple, Callable, Coroutine
import traceback
from src.common.logger_manager import get_logger
from src.plugins.chat.message import MessageRecv
from src.plugins.chat.chat_stream import chat_manager
import math
from src.plugins.heartFC_chat.heartFC_chat import HeartFChatting
from src.plugins.heartFC_chat.normal_chat import NormalChat
from src.heart_flow.mai_state_manager import MaiStateInfo
from src.heart_flow.chat_state_info import ChatState, ChatStateInfo
from src.heart_flow.sub_mind import SubMind
from .utils_chat import get_chat_type_and_target_info
from .interest_chatting import InterestChatting
# 定义常量 (从 interest.py 移动过来)
MAX_INTEREST = 15.0
logger = get_logger("sub_heartflow")
PROBABILITY_INCREASE_RATE_PER_SECOND = 0.1
PROBABILITY_DECREASE_RATE_PER_SECOND = 0.1
MAX_REPLY_PROBABILITY = 1
class InterestChatting:
def __init__(
self,
decay_rate=global_config.default_decay_rate_per_second,
max_interest=MAX_INTEREST,
trigger_threshold=global_config.reply_trigger_threshold,
max_probability=MAX_REPLY_PROBABILITY,
):
# 基础属性初始化
self.interest_level: float = 0.0
self.decay_rate_per_second: float = decay_rate
self.max_interest: float = max_interest
self.trigger_threshold: float = trigger_threshold
self.max_reply_probability: float = max_probability
self.is_above_threshold: bool = False
# 任务相关属性初始化
self.update_task: Optional[asyncio.Task] = None
self._stop_event = asyncio.Event()
self._task_lock = asyncio.Lock()
self._is_running = False
self.interest_dict: Dict[str, tuple[MessageRecv, float, bool]] = {}
self.update_interval = 1.0
self.above_threshold = False
self.start_hfc_probability = 0.0
async def initialize(self):
async with self._task_lock:
if self._is_running:
logger.debug("后台兴趣更新任务已在运行中。")
return
# 清理已完成或已取消的任务
if self.update_task and (self.update_task.done() or self.update_task.cancelled()):
self.update_task = None
if not self.update_task:
self._stop_event.clear()
self._is_running = True
self.update_task = asyncio.create_task(self._run_update_loop(self.update_interval))
logger.debug("后台兴趣更新任务已创建并启动。")
def add_interest_dict(self, message: MessageRecv, interest_value: float, is_mentioned: bool):
"""添加消息到兴趣字典
参数:
message: 接收到的消息
interest_value: 兴趣值
is_mentioned: 是否被提及
功能:
1. 将消息添加到兴趣字典
2. 更新最后交互时间
3. 如果字典长度超过10删除最旧的消息
"""
# 添加新消息
self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned)
# 如果字典长度超过10删除最旧的消息
if len(self.interest_dict) > 10:
oldest_key = next(iter(self.interest_dict))
self.interest_dict.pop(oldest_key)
async def _calculate_decay(self):
"""计算兴趣值的衰减
参数:
current_time: 当前时间戳
处理逻辑:
1. 计算时间差
2. 处理各种异常情况(负值/零值)
3. 正常计算衰减
4. 更新最后更新时间
"""
# 处理极小兴趣值情况
if self.interest_level < 1e-9:
self.interest_level = 0.0
return
# 异常情况处理
if self.decay_rate_per_second <= 0:
logger.warning(f"衰减率({self.decay_rate_per_second})无效重置兴趣值为0")
self.interest_level = 0.0
return
# 正常衰减计算
try:
decay_factor = math.pow(self.decay_rate_per_second, self.update_interval)
self.interest_level *= decay_factor
except ValueError as e:
logger.error(
f"衰减计算错误: {e} 参数: 衰减率={self.decay_rate_per_second} 时间差={self.update_interval} 当前兴趣={self.interest_level}"
)
self.interest_level = 0.0
async def _update_reply_probability(self):
self.above_threshold = self.interest_level >= self.trigger_threshold
if self.above_threshold:
self.start_hfc_probability += PROBABILITY_INCREASE_RATE_PER_SECOND
else:
if self.start_hfc_probability > 0:
self.start_hfc_probability = max(0, self.start_hfc_probability - PROBABILITY_DECREASE_RATE_PER_SECOND)
async def increase_interest(self, value: float):
self.interest_level += value
self.interest_level = min(self.interest_level, self.max_interest)
async def decrease_interest(self, value: float):
self.interest_level -= value
self.interest_level = max(self.interest_level, 0.0)
async def get_interest(self) -> float:
return self.interest_level
async def get_state(self) -> dict:
interest = self.interest_level # 直接使用属性值
return {
"interest_level": round(interest, 2),
"start_hfc_probability": round(self.start_hfc_probability, 4),
"above_threshold": self.above_threshold,
}
# --- 新增后台更新任务相关方法 ---
async def _run_update_loop(self, update_interval: float = 1.0):
"""后台循环,定期更新兴趣和回复概率。"""
try:
while not self._stop_event.is_set():
try:
if self.interest_level != 0:
await self._calculate_decay()
await self._update_reply_probability()
# 等待下一个周期或停止事件
await asyncio.wait_for(self._stop_event.wait(), timeout=update_interval)
except asyncio.TimeoutError:
# 正常超时,继续循环
continue
except Exception as e:
logger.error(f"InterestChatting 更新循环出错: {e}")
logger.error(traceback.format_exc())
# 防止错误导致CPU飙升稍作等待
await asyncio.sleep(5)
except asyncio.CancelledError:
logger.info("InterestChatting 更新循环被取消。")
finally:
self._is_running = False
logger.info("InterestChatting 更新循环已停止。")
async def stop_updates(self):
"""停止后台更新任务,使用锁确保并发安全"""
async with self._task_lock:
if not self._is_running:
logger.debug("后台兴趣更新任务未运行。")
return
logger.info("正在停止 InterestChatting 后台更新任务...")
self._stop_event.set()
if self.update_task and not self.update_task.done():
try:
# 等待任务结束,设置超时
await asyncio.wait_for(self.update_task, timeout=5.0)
logger.info("InterestChatting 后台更新任务已成功停止。")
except asyncio.TimeoutError:
logger.warning("停止 InterestChatting 后台任务超时,尝试取消...")
self.update_task.cancel()
try:
await self.update_task # 等待取消完成
except asyncio.CancelledError:
logger.info("InterestChatting 后台更新任务已被取消。")
except Exception as e:
logger.error(f"停止 InterestChatting 后台任务时发生异常: {e}")
finally:
self.update_task = None
self._is_running = False
# --- 结束 新增方法 ---
class SubHeartflow:
def __init__(

View File

@ -83,6 +83,17 @@ class SubHeartflowManager:
request_type="subheartflow_state_eval", # 保留特定的请求类型
)
async def force_change_state(self, subflow_id: Any, target_state: ChatState) -> bool:
"""强制改变指定子心流的状态"""
async with self._lock:
subflow = self.subheartflows.get(subflow_id)
if not subflow:
logger.warning(f"[强制状态转换]尝试转换不存在的子心流{subflow_id}{target_state.value}")
return False
await subflow.change_chat_state(target_state)
logger.info(f"[强制状态转换]子心流 {subflow_id} 已转换到 {target_state.value}")
return True
def get_all_subheartflows(self) -> List["SubHeartflow"]:
"""获取所有当前管理的 SubHeartflow 实例列表 (快照)。"""
return list(self.subheartflows.values())
@ -92,7 +103,7 @@ class SubHeartflowManager:
Args:
subheartflow_id: 子心流唯一标识符
# mai_states 参数已被移除,使用 self.mai_state_info
mai_states 参数已被移除使用 self.mai_state_info
Returns:
成功返回SubHeartflow实例失败返回None
@ -165,7 +176,7 @@ class SubHeartflowManager:
def get_inactive_subheartflows(self, max_age_seconds=INACTIVE_THRESHOLD_SECONDS):
"""识别并返回需要清理的不活跃(处于ABSENT状态超过一小时)子心流(id, 原因)"""
current_time = time.time()
_current_time = time.time()
flows_to_stop = []
for subheartflow_id, subheartflow in list(self.subheartflows.items()):
@ -173,9 +184,8 @@ class SubHeartflowManager:
if state != ChatState.ABSENT:
continue
subheartflow.update_last_chat_state_time()
absent_last_time = subheartflow.chat_state_last_time
if max_age_seconds and (current_time - absent_last_time) > max_age_seconds:
flows_to_stop.append(subheartflow_id)
_absent_last_time = subheartflow.chat_state_last_time
flows_to_stop.append(subheartflow_id)
return flows_to_stop
@ -662,12 +672,12 @@ class SubHeartflowManager:
"""处理来自 HeartFChatting 的连续无回复信号 (通过 partial 绑定 ID)"""
# 注意:这里不需要再获取锁,因为 sbhf_focus_into_absent 内部会处理锁
logger.debug(f"[管理器 HFC 处理器] 接收到来自 {subheartflow_id} 的 HFC 无回复信号")
await self.sbhf_focus_into_absent(subheartflow_id)
await self.sbhf_focus_into_absent_or_chat(subheartflow_id)
# --- 结束新增 --- #
# --- 新增:处理来自 HeartFChatting 的状态转换请求 --- #
async def sbhf_focus_into_absent(self, subflow_id: Any):
async def sbhf_focus_into_absent_or_chat(self, subflow_id: Any):
"""
接收来自 HeartFChatting 的请求将特定子心流的状态转换为 ABSENT CHAT
通常在连续多次 "no_reply" 后被调用
@ -719,6 +729,8 @@ class SubHeartflowManager:
f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 尝试转换为 {target_state.value} ({log_reason})"
)
try:
# 从HFC到CHAT时清空兴趣字典
subflow.clear_interest_dict()
await subflow.change_chat_state(target_state)
final_state = subflow.chat_state.chat_status
if final_state == target_state:
@ -842,3 +854,52 @@ class SubHeartflowManager:
# --- 结束新增 --- #
# --- 结束新增:处理来自 HeartFChatting 的状态转换请求 --- #
# 临时函数用于GUI切换有api后删除
# async def detect_command_from_gui(self):
# """检测来自GUI的命令"""
# command_file = Path("temp_command/gui_command.json")
# if not command_file.exists():
# return
# try:
# # 读取并解析命令文件
# command_data = json.loads(command_file.read_text())
# subflow_id = command_data.get("subflow_id")
# target_state = command_data.get("target_state")
# if not subflow_id or not target_state:
# logger.warning("GUI命令文件格式不正确缺少必要字段")
# return
# # 尝试转换为ChatState枚举
# try:
# target_state_enum = ChatState[target_state.upper()]
# except KeyError:
# logger.warning(f"无效的目标状态: {target_state}")
# command_file.unlink()
# return
# # 执行状态转换
# await self.force_change_by_gui(subflow_id, target_state_enum)
# # 转换成功后删除文件
# command_file.unlink()
# logger.debug(f"已处理GUI命令并删除命令文件: {command_file}")
# except json.JSONDecodeError:
# logger.warning("GUI命令文件不是有效的JSON格式")
# except Exception as e:
# logger.error(f"处理GUI命令时发生错误: {e}", exc_info=True)
# async def force_change_by_gui(self, subflow_id: Any, target_state: ChatState):
# """强制改变指定子心流的状态"""
# async with self._lock:
# subflow = self.subheartflows.get(subflow_id)
# if not subflow:
# logger.warning(f"[强制状态转换] 尝试转换不存在的子心流 {subflow_id} 到 {target_state.value}")
# return
# await subflow.change_chat_state(target_state)
# logger.info(f"[强制状态转换] 成功将 {subflow_id} 的状态转换为 {target_state.value}")
# --- 结束新增 --- #

View File

@ -18,6 +18,7 @@ from .plugins.remote import heartbeat_thread # noqa: F401
from .individuality.individuality import Individuality
from .common.server import global_server
from rich.traceback import install
from .api.main import start_api_server
install(extra_lines=3)
@ -54,6 +55,9 @@ class MainSystem:
self.llm_stats.start()
logger.success("LLM统计功能启动成功")
# 启动API服务器
start_api_server()
logger.success("API服务器启动成功")
# 初始化表情管理器
emoji_manager.initialize()
logger.success("表情包管理器初始化成功")

View File

@ -1 +0,0 @@

View File

@ -1,19 +0,0 @@
from fastapi import APIRouter, HTTPException
from rich.traceback import install
install(extra_lines=3)
# 创建APIRouter而不是FastAPI实例
router = APIRouter()
@router.post("/reload-config")
async def reload_config():
try: # TODO: 实现配置重载
# bot_config_path = os.path.join(BotConfig.get_config_dir(), "bot_config.toml")
# BotConfig.reload_config(config_path=bot_config_path)
return {"message": "TODO: 实现配置重载", "status": "unimplemented"}
except FileNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e)) from e
except Exception as e:
raise HTTPException(status_code=500, detail=f"重载配置时发生错误: {str(e)}") from e

View File

@ -1,4 +0,0 @@
import requests
response = requests.post("http://localhost:8080/api/reload-config")
print(response.json())

View File

@ -1,11 +1,13 @@
from dataclasses import dataclass
import json
import os
import math
from typing import Dict, List, Tuple
import numpy as np
import pandas as pd
import tqdm
# import tqdm
import faiss
from .llm_client import LLMClient
@ -25,9 +27,39 @@ from rich.progress import (
)
install(extra_lines=3)
ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
TOTAL_EMBEDDING_TIMES = 3 # 统计嵌入次数
# 嵌入模型测试字符串,测试模型一致性,来自开发群的聊天记录
# 这些字符串的嵌入结果应该是固定的,不能随时间变化
EMBEDDING_TEST_STRINGS = [
"阿卡伊真的太好玩了,神秘性感大女同等着你",
"你怎么知道我arc12.64了",
"我是蕾缪乐小姐的狗",
"关注Oct谢谢喵",
"不是w6我不草",
"关注千石可乐谢谢喵",
"来玩CLANNADAIR樱之诗樱之刻谢谢喵",
"关注墨梓柒谢谢喵",
"Ciallo~",
"来玩巧克甜恋谢谢喵",
"水印",
"我也在纠结晚饭,铁锅炒鸡听着就香!",
"test你妈喵",
]
EMBEDDING_TEST_FILE = os.path.join(ROOT_PATH, "data", "embedding_model_test.json")
EMBEDDING_SIM_THRESHOLD = 0.99
def cosine_similarity(a, b):
# 计算余弦相似度
dot = sum(x * y for x, y in zip(a, b))
norm_a = math.sqrt(sum(x * x for x in a))
norm_b = math.sqrt(sum(x * x for x in b))
if norm_a == 0 or norm_b == 0:
return 0.0
return dot / (norm_a * norm_b)
@dataclass
class EmbeddingStoreItem:
@ -64,6 +96,46 @@ class EmbeddingStore:
def _get_embedding(self, s: str) -> List[float]:
return self.llm_client.send_embedding_request(global_config["embedding"]["model"], s)
def get_test_file_path(self):
return EMBEDDING_TEST_FILE
def save_embedding_test_vectors(self):
"""保存测试字符串的嵌入到本地"""
test_vectors = {}
for idx, s in enumerate(EMBEDDING_TEST_STRINGS):
test_vectors[str(idx)] = self._get_embedding(s)
with open(self.get_test_file_path(), "w", encoding="utf-8") as f:
json.dump(test_vectors, f, ensure_ascii=False, indent=2)
def load_embedding_test_vectors(self):
"""加载本地保存的测试字符串嵌入"""
path = self.get_test_file_path()
if not os.path.exists(path):
return None
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def check_embedding_model_consistency(self):
"""校验当前模型与本地嵌入模型是否一致"""
local_vectors = self.load_embedding_test_vectors()
if local_vectors is None:
logger.warning("未检测到本地嵌入模型测试文件,将保存当前模型的测试嵌入。")
self.save_embedding_test_vectors()
return True
for idx, s in enumerate(EMBEDDING_TEST_STRINGS):
local_emb = local_vectors.get(str(idx))
if local_emb is None:
logger.warning("本地嵌入模型测试文件缺失部分测试字符串,将重新保存。")
self.save_embedding_test_vectors()
return True
new_emb = self._get_embedding(s)
sim = cosine_similarity(local_emb, new_emb)
if sim < EMBEDDING_SIM_THRESHOLD:
logger.error("嵌入模型一致性校验失败")
return False
logger.info("嵌入模型一致性校验通过。")
return True
def batch_insert_strs(self, strs: List[str], times: int) -> None:
"""向库中存入字符串"""
total = len(strs)
@ -123,11 +195,25 @@ class EmbeddingStore:
"""从文件中加载"""
if not os.path.exists(self.embedding_file_path):
raise Exception(f"文件{self.embedding_file_path}不存在")
logger.info(f"正在从文件{self.embedding_file_path}中加载{self.namespace}嵌入库")
data_frame = pd.read_parquet(self.embedding_file_path, engine="pyarrow")
for _, row in tqdm.tqdm(data_frame.iterrows(), total=len(data_frame)):
self.store[row["hash"]] = EmbeddingStoreItem(row["hash"], row["embedding"], row["str"])
total = len(data_frame)
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TaskProgressColumn(),
MofNCompleteColumn(),
"",
TimeElapsedColumn(),
"<",
TimeRemainingColumn(),
transient=False,
) as progress:
task = progress.add_task("加载嵌入库", total=total)
for _, row in data_frame.iterrows():
self.store[row["hash"]] = EmbeddingStoreItem(row["hash"], row["embedding"], row["str"])
progress.update(task, advance=1)
logger.info(f"{self.namespace}嵌入库加载成功")
try:
@ -216,6 +302,17 @@ class EmbeddingManager:
)
self.stored_pg_hashes = set()
def check_all_embedding_model_consistency(self):
"""对所有嵌入库做模型一致性校验"""
for store in [
self.paragraphs_embedding_store,
self.entities_embedding_store,
self.relation_embedding_store,
]:
if not store.check_embedding_model_consistency():
return False
return True
def _store_pg_into_embedding(self, raw_paragraphs: Dict[str, str]):
"""将段落编码存入Embedding库"""
self.paragraphs_embedding_store.batch_insert_strs(list(raw_paragraphs.values()), times=1)
@ -239,6 +336,8 @@ class EmbeddingManager:
def load_from_file(self):
"""从文件加载"""
if not self.check_all_embedding_model_consistency():
raise Exception("嵌入模型与本地存储不一致,请检查模型设置或清空嵌入库后重试。")
self.paragraphs_embedding_store.load_from_file()
self.entities_embedding_store.load_from_file()
self.relation_embedding_store.load_from_file()
@ -250,6 +349,8 @@ class EmbeddingManager:
raw_paragraphs: Dict[str, str],
triple_list_data: Dict[str, List[List[str]]],
):
if not self.check_all_embedding_model_consistency():
raise Exception("嵌入模型与本地存储不一致,请检查模型设置或清空嵌入库后重试。")
"""存储新的数据集"""
self._store_pg_into_embedding(raw_paragraphs)
self._store_ent_into_embedding(triple_list_data)

View File

@ -215,9 +215,11 @@ class PersonInfoManager:
if old_name:
qv_name_prompt += f"你之前叫他{old_name},是因为{old_reason}"
qv_name_prompt += f"\n其他取名的要求是:{request}"
qv_name_prompt += f"\n其他取名的要求是:{request},不要太浮夸"
qv_name_prompt += "\n请根据以上用户信息想想你叫他什么比较好请最好使用用户的qq昵称可以稍作修改"
qv_name_prompt += (
"\n请根据以上用户信息想想你叫他什么比较好不要太浮夸请最好使用用户的qq昵称可以稍作修改"
)
if existing_names:
qv_name_prompt += f"\n请注意,以下名称已被使用,不要使用以下昵称:{existing_names}\n"
qv_name_prompt += "请用json给出你的想法并给出理由示例如下"

View File

@ -15,33 +15,67 @@ remote_log_config = LogConfig(
)
logger = get_module_logger("remote", config=remote_log_config)
# UUID文件路径
UUID_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "client_uuid.json")
# --- 使用向上导航的方式定义路径 ---
# 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")
# --- 路径定义结束 ---
# 生成或获取客户端唯一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
# --- 目录检查结束 ---
# 检查是否已经有保存的UUID
if os.path.exists(UUID_FILE):
try:
with open(UUID_FILE, "r") as f:
with open(UUID_FILE, "r", encoding="utf-8") as f: # 指定 encoding
data = json.load(f)
if "client_id" in data:
# print("从本地文件读取客户端ID")
logger.debug(f"从本地文件读取客户端ID: {UUID_FILE}")
return data["client_id"]
except (json.JSONDecodeError, IOError) as e:
print(f"读取UUID文件出错: {e}将生成新的UUID")
logger.warning(f"读取UUID文件 {UUID_FILE} 出错: {e}将生成新的UUID")
except Exception as e: # 捕捉其他可能的异常
logger.error(f"读取UUID文件 {UUID_FILE} 时发生未知错误: {e}")
# 如果没有保存的UUID或读取出错则生成新的
client_id = generate_unique_id()
logger.info(f"生成新的客户端ID: {client_id}")
# 保存UUID到文件
try:
with open(UUID_FILE, "w") as f:
json.dump({"client_id": client_id}, f)
logger.info("已保存新生成的客户端ID到本地文件")
# 再次确认目录存在 (虽然理论上前面已创建,但更保险)
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时出错: {e}")
logger.error(f"保存UUID时出错: {UUID_FILE} - {e}")
except Exception as e: # 捕捉其他可能的异常
logger.error(f"保存UUID文件 {UUID_FILE} 时发生未知错误: {e}")
return client_id

View File

@ -69,8 +69,8 @@ nonebot-qq="http://127.0.0.1:18002/api/message"
allow_focus_mode = true # 是否允许专注聊天状态
# 是否启用heart_flowC(HFC)模式
# 启用后麦麦会自主选择进入heart_flowC模式(持续一段时间进行主动的观察和回复并给出回复比较消耗token
base_normal_chat_num = 3 # 最多允许多少个群进行普通聊天
base_focused_chat_num = 2 # 最多允许多少个群进行专注聊天
base_normal_chat_num = 8 # 最多允许多少个群进行普通聊天
base_focused_chat_num = 5 # 最多允许多少个群进行专注聊天
observation_context_size = 15 # 观察到的最长上下文大小,建议15太短太长都会导致脑袋尖尖
message_buffer = true # 启用消息缓冲器?启用此项以解决消息的拆分问题,但会使麦麦的回复延迟

View File

@ -38,8 +38,8 @@ synonym_threshold = 0.8 # 同义词阈值(相似度高于此阈值的词语
provider = "siliconflow" # 服务提供商
model = "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B" # 模型名称
[info_extraction]
workers = 10
[info_extraction]
workers = 3 # 实体提取同时执行线程数非Pro模型不要设置超过5
[qa.params]
# QA参数配置

View File

@ -1,26 +0,0 @@
@echo off
CHCP 65001 > nul
setlocal enabledelayedexpansion
REM 查找venv虚拟环境
set "venv_path=%~dp0venv\Scripts\activate.bat"
if not exist "%venv_path%" (
echo 错误: 未找到虚拟环境请确保venv目录存在
pause
exit /b 1
)
REM 激活虚拟环境
call "%venv_path%"
if %ERRORLEVEL% neq 0 (
echo 错误: 虚拟环境激活失败
pause
exit /b 1
)
echo 虚拟环境已激活,正在启动 GUI...
REM 运行 Python 脚本
python scripts/interest_monitor_gui.py
pause