mirror of https://github.com/Mai-with-u/MaiBot.git
Merge branch 'dev' of https://github.com/Dax233/MaiMBot into groupnickname
commit
c26a0bd53b
|
|
@ -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
36
bot.py
|
|
@ -19,6 +19,11 @@ import atexit
|
|||
|
||||
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")
|
||||
|
|
@ -241,6 +246,7 @@ def raw_main():
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = 0 # 用于记录程序最终的退出状态
|
||||
try:
|
||||
# 获取MainSystem实例
|
||||
main_system = raw_main()
|
||||
|
|
@ -256,13 +262,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) # <--- 使用记录的退出码
|
||||
|
|
|
|||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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服务器
|
||||
# ...如有更多模块,继续添加...
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
# )
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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__(
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
||||
# --- 结束新增 --- #
|
||||
|
|
|
|||
|
|
@ -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("表情包管理器初始化成功")
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -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
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import requests
|
||||
|
||||
response = requests.post("http://localhost:8080/api/reload-config")
|
||||
print(response.json())
|
||||
|
|
@ -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我不草",
|
||||
"关注千石可乐谢谢喵",
|
||||
"来玩CLANNAD,AIR,樱之诗,樱之刻谢谢喵",
|
||||
"关注墨梓柒谢谢喵",
|
||||
"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)
|
||||
|
|
|
|||
|
|
@ -216,9 +216,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给出你的想法,并给出理由,示例如下:"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 # 启用消息缓冲器?启用此项以解决消息的拆分问题,但会使麦麦的回复延迟
|
||||
|
|
|
|||
|
|
@ -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参数配置
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue