mirror of https://github.com/Mai-with-u/MaiBot.git
Merge branch 'PFC-test' of https://github.com/Dax233/MaiMBot into G-Test
commit
6620d91f8e
|
|
@ -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服务器
|
||||
# ...如有更多模块,继续添加...
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -710,6 +710,18 @@ class BotConfig:
|
|||
if config.INNER_VERSION in SpecifierSet(">=1.6.1.5"):
|
||||
config.api_polling_max_retries = experimental_config.get("api_polling_max_retries", config.api_polling_max_retries)
|
||||
|
||||
def idle_conversation(parent: dict):
|
||||
idle_conversation_config = parent["idle_conversation"]
|
||||
if config.INNER_VERSION in SpecifierSet(">=1.6.1.6"):
|
||||
config.enable_idle_conversation = idle_conversation_config.get(
|
||||
"enable_idle_conversation", config.enable_idle_conversation
|
||||
)
|
||||
config.idle_check_interval = idle_conversation_config.get(
|
||||
"idle_check_interval", config.idle_check_interval
|
||||
)
|
||||
config.min_idle_time = idle_conversation_config.get("min_idle_time", config.min_idle_time)
|
||||
config.max_idle_time = idle_conversation_config.get("max_idle_time", config.max_idle_time)
|
||||
|
||||
# 版本表达式:>=1.0.0,<2.0.0
|
||||
# 允许字段:func: method, support: str, notice: str, necessary: bool
|
||||
# 如果使用 notice 字段,在该组配置加载时,会展示该字段对用户的警示
|
||||
|
|
@ -744,6 +756,7 @@ class BotConfig:
|
|||
"normal_chat": {"func": normal_chat, "support": ">=1.6.0", "necessary": False},
|
||||
"focus_chat": {"func": focus_chat, "support": ">=1.6.0", "necessary": False},
|
||||
"group_nickname": {"func": group_nickname, "support": ">=1.6.1.1", "necessary": False},
|
||||
"idle_conversation": {"func": idle_conversation, "support": ">=1.6.1.6", "necessary": False},
|
||||
}
|
||||
|
||||
# 原地修改,将 字符串版本表达式 转换成 版本对象
|
||||
|
|
|
|||
|
|
@ -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,24 +1,23 @@
|
|||
import time
|
||||
from typing import Tuple, Optional
|
||||
from .pfc_utils import retrieve_contextual_info
|
||||
import traceback
|
||||
from typing import Tuple, Optional, Dict, Any, List
|
||||
|
||||
# import jieba # 如果需要旧版知识库的回退,可能需要
|
||||
# import re # 如果需要旧版知识库的回退,可能需要
|
||||
from src.common.logger_manager import get_logger
|
||||
from src.individuality.individuality import Individuality
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
from ..models.utils_model import LLMRequest
|
||||
from ...config.config import global_config
|
||||
|
||||
# 确保导入路径正确
|
||||
from .pfc_utils import get_items_from_json, retrieve_contextual_info
|
||||
from .chat_observer import ChatObserver
|
||||
from .pfc_utils import get_items_from_json
|
||||
from src.individuality.individuality import Individuality
|
||||
from .observation_info import ObservationInfo
|
||||
from .conversation_info import ConversationInfo
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
|
||||
|
||||
logger = get_logger("pfc_action_planner")
|
||||
|
||||
|
||||
# --- 定义 Prompt 模板 ---
|
||||
# --- 定义 Prompt 模板 ---
|
||||
|
||||
# Prompt(1): 首次回复或非连续回复时的决策 Prompt
|
||||
PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以回复,可以倾听,可以调取知识,甚至可以屏蔽对方:
|
||||
|
|
@ -41,7 +40,7 @@ PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊,
|
|||
------
|
||||
可选行动类型以及解释:
|
||||
listening: 倾听对方发言,当你认为对方话才说到一半,发言明显未结束时选择
|
||||
direct_reply: 直接回复对方
|
||||
direct_reply: 直接回复对方 (当有新消息需要处理时,通常应选择此项)
|
||||
rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择
|
||||
end_conversation: 结束对话,对方长时间没回复或者当你觉得对话告一段落时可以选择
|
||||
block_and_ignore: 更加极端的结束对话方式,直接结束对话并在一段时间内无视对方所有发言(屏蔽),当对话让你感到十分不适,或你遭到各类骚扰时选择
|
||||
|
|
@ -73,7 +72,7 @@ PROMPT_FOLLOW_UP = """{persona_text}。现在你在参与一场QQ私聊,刚刚
|
|||
{retrieved_memory_str}
|
||||
------
|
||||
可选行动类型以及解释:
|
||||
wait: 暂时不说话,留给对方交互空间,等待对方回复(尤其是在你刚发言后、或上次发言因重复、发言过多被拒时、或不确定做什么时,这是不错的选择)
|
||||
wait: 暂时不说话,留给对方交互空间,等待对方回复(尤其是在你刚发言后、或上次发言因重复、发言过多被拒时、或不确定做什么时,这是不错的选择)。**重要:仅当没有未读消息时才能选择此项。**
|
||||
listening: 倾听对方发言(虽然你刚发过言,但如果对方立刻回复且明显话没说完,可以选择这个)
|
||||
send_new_message: 发送一条新消息继续对话,允许适当的追问、补充、深入话题,或开启相关新话题。**但是避免在因重复被拒后立即使用,也不要在对方没有回复的情况下过多的“消息轰炸”或重复发言**
|
||||
rethink_goal: 思考一个对话目标,当你觉得目前对话需要目标,或当前目标不再适用,或话题卡住时选择。注意私聊的环境是灵活的,有可能需要经常选择
|
||||
|
|
@ -107,274 +106,113 @@ PROMPT_END_DECISION = """{persona_text}。刚刚你决定结束一场 QQ 私聊
|
|||
注意:请严格按照 JSON 格式输出,不要包含任何其他内容。"""
|
||||
|
||||
|
||||
# ActionPlanner 类定义,顶格
|
||||
class ActionPlanner:
|
||||
"""行动规划器"""
|
||||
|
||||
def __init__(self, stream_id: str, private_name: str):
|
||||
self.llm = LLMRequest(
|
||||
model=global_config.llm_PFC_action_planner,
|
||||
temperature=global_config.llm_PFC_action_planner["temp"],
|
||||
max_tokens=1500,
|
||||
request_type="action_planning",
|
||||
)
|
||||
"""初始化行动规划器"""
|
||||
self.stream_id = stream_id
|
||||
self.private_name = private_name
|
||||
# 初始化 LLM 请求对象
|
||||
try:
|
||||
llm_config = global_config.llm_PFC_action_planner
|
||||
if not isinstance(llm_config, dict):
|
||||
raise TypeError(f"LLM config 'llm_PFC_action_planner' is not a dictionary: {llm_config}")
|
||||
|
||||
self.llm = LLMRequest(
|
||||
model=llm_config,
|
||||
temperature=llm_config.get("temp", 0.7),
|
||||
max_tokens=1500,
|
||||
request_type="action_planning",
|
||||
)
|
||||
except TypeError as e:
|
||||
logger.error(f"[私聊][{self.private_name}] 初始化 LLMRequest 时配置错误: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}] 初始化 LLMRequest 时发生未知错误: {e}")
|
||||
raise
|
||||
|
||||
# 获取个性化信息和机器人名称
|
||||
self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
|
||||
self.name = global_config.BOT_NICKNAME
|
||||
self.private_name = private_name
|
||||
# 获取 ChatObserver 实例 (单例模式)
|
||||
self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
|
||||
|
||||
# 修改 plan 方法签名,增加 last_successful_reply_action 参数
|
||||
async def plan(
|
||||
self,
|
||||
observation_info: ObservationInfo,
|
||||
conversation_info: ConversationInfo,
|
||||
last_successful_reply_action: Optional[str],
|
||||
) -> Tuple[str, str]:
|
||||
"""规划下一步行动
|
||||
"""
|
||||
规划下一步行动。
|
||||
|
||||
Args:
|
||||
observation_info: 决策信息
|
||||
conversation_info: 对话信息
|
||||
last_successful_reply_action: 上一次成功的回复动作类型 ('direct_reply' 或 'send_new_message' 或 None)
|
||||
observation_info: 观察信息,包含聊天记录、未读消息等。
|
||||
conversation_info: 对话信息,包含目标、历史动作等。
|
||||
last_successful_reply_action: 上一次成功的回复动作类型 ('direct_reply' 或 'send_new_message' 或 None)。
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: (行动类型, 行动原因)
|
||||
Tuple[str, str]: (规划的行动类型, 行动原因)。
|
||||
"""
|
||||
# --- 获取 Bot 上次发言时间信息 ---
|
||||
# (这部分逻辑不变)
|
||||
time_since_last_bot_message_info = ""
|
||||
logger.info(f"[私聊][{self.private_name}] 开始规划行动...")
|
||||
plan_start_time = time.time()
|
||||
|
||||
# --- 1. 准备 Prompt 输入信息 ---
|
||||
try:
|
||||
bot_id = str(global_config.BOT_QQ)
|
||||
if hasattr(observation_info, "chat_history") and observation_info.chat_history:
|
||||
for i in range(len(observation_info.chat_history) - 1, -1, -1):
|
||||
msg = observation_info.chat_history[i]
|
||||
if not isinstance(msg, dict):
|
||||
continue
|
||||
sender_info = msg.get("user_info", {})
|
||||
sender_id = str(sender_info.get("user_id")) if isinstance(sender_info, dict) else None
|
||||
msg_time = msg.get("time")
|
||||
if sender_id == bot_id and msg_time:
|
||||
time_diff = time.time() - msg_time
|
||||
if time_diff < 60.0:
|
||||
time_since_last_bot_message_info = (
|
||||
f"提示:你上一条成功发送的消息是在 {time_diff:.1f} 秒前。\n"
|
||||
)
|
||||
break
|
||||
else:
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}]Observation info chat history is empty or not available for bot time check."
|
||||
)
|
||||
except AttributeError:
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}]ObservationInfo object might not have chat_history attribute yet for bot time check."
|
||||
time_since_last_bot_message_info = self._get_bot_last_speak_time_info(observation_info)
|
||||
timeout_context = self._get_timeout_context(conversation_info)
|
||||
goals_str = self._build_goals_string(conversation_info)
|
||||
chat_history_text = await self._build_chat_history_text(observation_info)
|
||||
persona_text = f"你的名字是{self.name},{self.personality_info}。"
|
||||
action_history_summary, last_action_context = self._build_action_history_context(conversation_info)
|
||||
retrieved_memory_str, retrieved_knowledge_str = await retrieve_contextual_info(
|
||||
chat_history_text, self.private_name
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[私聊][{self.private_name}]获取 Bot 上次发言时间时出错: {e}")
|
||||
|
||||
# --- 获取超时提示信息 ---
|
||||
# (这部分逻辑不变)
|
||||
timeout_context = ""
|
||||
try:
|
||||
if hasattr(conversation_info, "goal_list") and conversation_info.goal_list:
|
||||
last_goal_dict = conversation_info.goal_list[-1]
|
||||
if isinstance(last_goal_dict, dict) and "goal" in last_goal_dict:
|
||||
last_goal_text = last_goal_dict["goal"]
|
||||
if isinstance(last_goal_text, str) and "分钟,思考接下来要做什么" in last_goal_text:
|
||||
try:
|
||||
timeout_minutes_text = last_goal_text.split(",")[0].replace("你等待了", "")
|
||||
timeout_context = f"重要提示:对方已经长时间({timeout_minutes_text})没有回复你的消息了(这可能代表对方繁忙/不想回复/没注意到你的消息等情况,或在对方看来本次聊天已告一段落),请基于此情况规划下一步。\n"
|
||||
except Exception:
|
||||
timeout_context = "重要提示:对方已经长时间没有回复你的消息了(这可能代表对方繁忙/不想回复/没注意到你的消息等情况,或在对方看来本次聊天已告一段落),请基于此情况规划下一步。\n"
|
||||
else:
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}]Conversation info goal_list is empty or not available for timeout check."
|
||||
)
|
||||
except AttributeError:
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}]ConversationInfo object might not have goal_list attribute yet for timeout check."
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}] (ActionPlanner) 检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str else '无'} / 知识: {'有' if retrieved_knowledge_str and '无相关知识' not in retrieved_knowledge_str and '出错' not in retrieved_knowledge_str else '无'}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[私聊][{self.private_name}]检查超时目标时出错: {e}")
|
||||
except Exception as prep_err:
|
||||
logger.error(f"[私聊][{self.private_name}] 准备 Prompt 输入时出错: {prep_err}")
|
||||
logger.error(traceback.format_exc())
|
||||
return "wait", f"准备行动规划输入时出错: {prep_err}"
|
||||
|
||||
# --- 构建通用 Prompt 参数 ---
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}]开始规划行动:当前目标: {getattr(conversation_info, 'goal_list', '不可用')}"
|
||||
)
|
||||
|
||||
# 构建对话目标 (goals_str)
|
||||
goals_str = ""
|
||||
# --- 2. 选择并格式化 Prompt ---
|
||||
try:
|
||||
if hasattr(conversation_info, "goal_list") and conversation_info.goal_list:
|
||||
for goal_reason in conversation_info.goal_list:
|
||||
if isinstance(goal_reason, dict):
|
||||
goal = goal_reason.get("goal", "目标内容缺失")
|
||||
reasoning = goal_reason.get("reasoning", "没有明确原因")
|
||||
else:
|
||||
goal = str(goal_reason)
|
||||
reasoning = "没有明确原因"
|
||||
|
||||
goal = str(goal) if goal is not None else "目标内容缺失"
|
||||
reasoning = str(reasoning) if reasoning is not None else "没有明确原因"
|
||||
goals_str += f"- 目标:{goal}\n 原因:{reasoning}\n"
|
||||
|
||||
if not goals_str:
|
||||
goals_str = "- 目前没有明确对话目标,请考虑设定一个。\n"
|
||||
if last_successful_reply_action in ["direct_reply", "send_new_message"]:
|
||||
prompt_template = PROMPT_FOLLOW_UP
|
||||
log_msg = "使用 PROMPT_FOLLOW_UP (追问决策)"
|
||||
else:
|
||||
goals_str = "- 目前没有明确对话目标,请考虑设定一个。\n"
|
||||
except AttributeError:
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}]ConversationInfo object might not have goal_list attribute yet."
|
||||
prompt_template = PROMPT_INITIAL_REPLY
|
||||
log_msg = "使用 PROMPT_INITIAL_REPLY (首次/非连续回复决策)"
|
||||
logger.debug(f"[私聊][{self.private_name}] {log_msg}")
|
||||
|
||||
prompt = prompt_template.format(
|
||||
persona_text=persona_text,
|
||||
goals_str=goals_str if goals_str.strip() else "- 目前没有明确对话目标,请考虑设定一个。",
|
||||
action_history_summary=action_history_summary,
|
||||
last_action_context=last_action_context,
|
||||
time_since_last_bot_message_info=time_since_last_bot_message_info,
|
||||
timeout_context=timeout_context,
|
||||
chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。",
|
||||
retrieved_memory_str=retrieved_memory_str if retrieved_memory_str else "无相关记忆。",
|
||||
retrieved_knowledge_str=retrieved_knowledge_str if retrieved_knowledge_str else "无相关知识。",
|
||||
)
|
||||
goals_str = "- 获取对话目标时出错。\n"
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]构建对话目标字符串时出错: {e}")
|
||||
goals_str = "- 构建对话目标时出错。\n"
|
||||
logger.debug(f"[私聊][{self.private_name}] 发送到LLM的最终提示词:\n------\n{prompt}\n------")
|
||||
except KeyError as fmt_key_err:
|
||||
logger.error(f"[私聊][{self.private_name}] 格式化 Prompt 时缺少键: {fmt_key_err}")
|
||||
return "wait", f"格式化 Prompt 时出错 (缺少键: {fmt_key_err})"
|
||||
except Exception as fmt_err:
|
||||
logger.error(f"[私聊][{self.private_name}] 格式化 Prompt 时发生未知错误: {fmt_err}")
|
||||
return "wait", f"格式化 Prompt 时出错: {fmt_err}"
|
||||
|
||||
# 获取聊天历史记录 (chat_history_text)
|
||||
try:
|
||||
if hasattr(observation_info, "chat_history") and observation_info.chat_history:
|
||||
chat_history_text = observation_info.chat_history_str
|
||||
if not chat_history_text:
|
||||
chat_history_text = "还没有聊天记录。\n"
|
||||
else:
|
||||
chat_history_text = "还没有聊天记录。\n"
|
||||
|
||||
if hasattr(observation_info, "new_messages_count") and observation_info.new_messages_count > 0:
|
||||
if hasattr(observation_info, "unprocessed_messages") and observation_info.unprocessed_messages:
|
||||
new_messages_list = observation_info.unprocessed_messages
|
||||
new_messages_str = await build_readable_messages(
|
||||
new_messages_list,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="relative",
|
||||
read_mark=0.0,
|
||||
)
|
||||
chat_history_text += (
|
||||
f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}]ObservationInfo has new_messages_count > 0 but unprocessed_messages is empty or missing."
|
||||
)
|
||||
except AttributeError:
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}]ObservationInfo object might be missing expected attributes for chat history."
|
||||
)
|
||||
chat_history_text = "获取聊天记录时出错。\n"
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]处理聊天记录时发生未知错误: {e}")
|
||||
chat_history_text = "处理聊天记录时出错。\n"
|
||||
|
||||
# 构建 Persona 文本 (persona_text)
|
||||
persona_text = f"你的名字是{self.name},{self.personality_info}。"
|
||||
|
||||
# 构建行动历史和上一次行动结果 (action_history_summary, last_action_context)
|
||||
# (这部分逻辑不变)
|
||||
action_history_summary = "你最近执行的行动历史:\n"
|
||||
last_action_context = "关于你【上一次尝试】的行动:\n"
|
||||
action_history_list = []
|
||||
try:
|
||||
if hasattr(conversation_info, "done_action") and conversation_info.done_action:
|
||||
action_history_list = conversation_info.done_action[-5:]
|
||||
else:
|
||||
logger.debug(f"[私聊][{self.private_name}]Conversation info done_action is empty or not available.")
|
||||
except AttributeError:
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}]ConversationInfo object might not have done_action attribute yet."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]访问行动历史时出错: {e}")
|
||||
|
||||
if not action_history_list:
|
||||
action_history_summary += "- 还没有执行过行动。\n"
|
||||
last_action_context += "- 这是你规划的第一个行动。\n"
|
||||
else:
|
||||
for i, action_data in enumerate(action_history_list):
|
||||
action_type = "未知"
|
||||
plan_reason = "未知"
|
||||
status = "未知"
|
||||
final_reason = ""
|
||||
action_time = ""
|
||||
|
||||
if isinstance(action_data, dict):
|
||||
action_type = action_data.get("action", "未知")
|
||||
plan_reason = action_data.get("plan_reason", "未知规划原因")
|
||||
status = action_data.get("status", "未知")
|
||||
final_reason = action_data.get("final_reason", "")
|
||||
action_time = action_data.get("time", "")
|
||||
elif isinstance(action_data, tuple):
|
||||
# 假设旧格式兼容
|
||||
if len(action_data) > 0:
|
||||
action_type = action_data[0]
|
||||
if len(action_data) > 1:
|
||||
plan_reason = action_data[1] # 可能是规划原因或最终原因
|
||||
if len(action_data) > 2:
|
||||
status = action_data[2]
|
||||
if status == "recall" and len(action_data) > 3:
|
||||
final_reason = action_data[3]
|
||||
elif status == "done" and action_type in ["direct_reply", "send_new_message"]:
|
||||
plan_reason = "成功发送" # 简化显示
|
||||
|
||||
reason_text = f", 失败/取消原因: {final_reason}" if final_reason else ""
|
||||
summary_line = f"- 时间:{action_time}, 尝试行动:'{action_type}', 状态:{status}{reason_text}"
|
||||
action_history_summary += summary_line + "\n"
|
||||
|
||||
if i == len(action_history_list) - 1:
|
||||
last_action_context += f"- 上次【规划】的行动是: '{action_type}'\n"
|
||||
last_action_context += f"- 当时规划的【原因】是: {plan_reason}\n"
|
||||
if status == "done":
|
||||
last_action_context += "- 该行动已【成功执行】。\n"
|
||||
# 记录这次成功的行动类型,供下次决策
|
||||
# self.last_successful_action_type = action_type # 不在这里记录,由 conversation 控制
|
||||
elif status == "recall":
|
||||
last_action_context += "- 但该行动最终【未能执行/被取消】。\n"
|
||||
if final_reason:
|
||||
last_action_context += f"- 【重要】失败/取消的具体原因是: “{final_reason}”\n"
|
||||
else:
|
||||
last_action_context += "- 【重要】失败/取消原因未明确记录。\n"
|
||||
# self.last_successful_action_type = None # 行动失败,清除记录
|
||||
else:
|
||||
last_action_context += f"- 该行动当前状态: {status}\n"
|
||||
# self.last_successful_action_type = None # 非完成状态,清除记录
|
||||
|
||||
retrieved_memory_str_planner, retrieved_knowledge_str_planner = await retrieve_contextual_info(
|
||||
chat_history_text, self.private_name
|
||||
)
|
||||
# Optional: 可以加一行日志确认结果,方便调试
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}] (ActionPlanner) 统一检索完成。记忆: {'有' if '回忆起' in retrieved_memory_str_planner else '无'} / 知识: {'有' if '出错' not in retrieved_knowledge_str_planner and '无相关知识' not in retrieved_knowledge_str_planner else '无'}"
|
||||
)
|
||||
|
||||
# --- 选择 Prompt ---
|
||||
if last_successful_reply_action in ["direct_reply", "send_new_message"]:
|
||||
prompt_template = PROMPT_FOLLOW_UP
|
||||
logger.debug(f"[私聊][{self.private_name}]使用 PROMPT_FOLLOW_UP (追问决策)")
|
||||
else:
|
||||
prompt_template = PROMPT_INITIAL_REPLY
|
||||
logger.debug(f"[私聊][{self.private_name}]使用 PROMPT_INITIAL_REPLY (首次/非连续回复决策)")
|
||||
|
||||
# --- 格式化最终的 Prompt ---
|
||||
prompt = prompt_template.format(
|
||||
persona_text=persona_text,
|
||||
goals_str=goals_str if goals_str.strip() else "- 目前没有明确对话目标,请考虑设定一个。",
|
||||
action_history_summary=action_history_summary,
|
||||
last_action_context=last_action_context,
|
||||
time_since_last_bot_message_info=time_since_last_bot_message_info,
|
||||
timeout_context=timeout_context,
|
||||
chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。",
|
||||
# knowledge_info_str=knowledge_info_str, # 移除了旧知识展示方式
|
||||
retrieved_memory_str=retrieved_memory_str_planner if retrieved_memory_str_planner else "无相关记忆。",
|
||||
retrieved_knowledge_str=retrieved_knowledge_str_planner
|
||||
if retrieved_knowledge_str_planner
|
||||
else "无相关知识。",
|
||||
)
|
||||
|
||||
logger.debug(f"[私聊][{self.private_name}]发送到LLM的最终提示词:\n------\n{prompt}\n------")
|
||||
# --- 3. 调用 LLM 进行初步规划 ---
|
||||
try:
|
||||
llm_start_time = time.time()
|
||||
content, _ = await self.llm.generate_response_async(prompt)
|
||||
logger.debug(f"[私聊][{self.private_name}]LLM (行动规划) 原始返回内容: {content}")
|
||||
llm_duration = time.time() - llm_start_time
|
||||
logger.debug(f"[私聊][{self.private_name}] LLM (行动规划) 耗时: {llm_duration:.3f} 秒, 原始返回: {content}")
|
||||
|
||||
# --- 初始行动规划解析 ---
|
||||
success, initial_result = get_items_from_json(
|
||||
content,
|
||||
self.private_name,
|
||||
|
|
@ -385,87 +223,254 @@ class ActionPlanner:
|
|||
|
||||
initial_action = initial_result.get("action", "wait")
|
||||
initial_reason = initial_result.get("reason", "LLM未提供原因,默认等待")
|
||||
logger.info(f"[私聊][{self.private_name}] LLM 初步规划行动: {initial_action}, 原因: {initial_reason}")
|
||||
except Exception as llm_err:
|
||||
logger.error(f"[私聊][{self.private_name}] 调用 LLM 或解析初步规划结果时出错: {llm_err}")
|
||||
logger.error(traceback.format_exc())
|
||||
return "wait", f"行动规划 LLM 调用或解析出错: {llm_err}"
|
||||
|
||||
# 检查是否需要进行结束对话决策 ---
|
||||
if initial_action == "end_conversation":
|
||||
logger.info(f"[私聊][{self.private_name}]初步规划结束对话,进入告别决策...")
|
||||
# --- 4. 处理特殊动作 (end_conversation) ---
|
||||
final_action = initial_action
|
||||
final_reason = initial_reason
|
||||
|
||||
# 使用新的 PROMPT_END_DECISION
|
||||
end_decision_prompt = PROMPT_END_DECISION.format(
|
||||
persona_text=persona_text, # 复用之前的 persona_text
|
||||
chat_history_text=chat_history_text, # 复用之前的 chat_history_text
|
||||
if initial_action == "end_conversation":
|
||||
try:
|
||||
final_action, final_reason = await self._handle_end_conversation_decision(
|
||||
persona_text, chat_history_text, initial_reason
|
||||
)
|
||||
except Exception as end_dec_err:
|
||||
logger.error(f"[私聊][{self.private_name}] 处理结束对话决策时出错: {end_dec_err}")
|
||||
logger.warning(f"[私聊][{self.private_name}] 结束决策出错,将按原计划执行 end_conversation")
|
||||
final_action = "end_conversation" # 保持原计划
|
||||
final_reason = initial_reason
|
||||
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}]发送到LLM的结束决策提示词:\n------\n{end_decision_prompt}\n------"
|
||||
)
|
||||
try:
|
||||
end_content, _ = await self.llm.generate_response_async(end_decision_prompt) # 再次调用LLM
|
||||
logger.debug(f"[私聊][{self.private_name}]LLM (结束决策) 原始返回内容: {end_content}")
|
||||
# --- [移除] 不再需要在这里检查 wait 动作的约束 ---
|
||||
# elif initial_action == "wait":
|
||||
# # ... (移除之前的检查逻辑) ...
|
||||
# final_action = "wait"
|
||||
# final_reason = initial_reason
|
||||
|
||||
# 解析结束决策的JSON
|
||||
end_success, end_result = get_items_from_json(
|
||||
end_content,
|
||||
self.private_name,
|
||||
"say_bye",
|
||||
"reason",
|
||||
default_values={"say_bye": "no", "reason": "结束决策LLM返回格式错误,默认不告别"},
|
||||
required_types={"say_bye": str, "reason": str}, # 明确类型
|
||||
)
|
||||
# --- 5. 验证最终行动类型 ---
|
||||
valid_actions = [
|
||||
"direct_reply",
|
||||
"send_new_message",
|
||||
"wait",
|
||||
"listening",
|
||||
"rethink_goal",
|
||||
"end_conversation",
|
||||
"block_and_ignore",
|
||||
"say_goodbye",
|
||||
]
|
||||
if final_action not in valid_actions:
|
||||
logger.warning(f"[私聊][{self.private_name}] LLM 返回了未知的行动类型: '{final_action}',强制改为 wait")
|
||||
final_reason = f"(原始行动'{final_action}'无效,已强制改为wait) {final_reason}"
|
||||
final_action = "wait" # 遇到无效动作,默认等待
|
||||
|
||||
say_bye_decision = end_result.get("say_bye", "no").lower() # 转小写方便比较
|
||||
end_decision_reason = end_result.get("reason", "未提供原因")
|
||||
plan_duration = time.time() - plan_start_time
|
||||
logger.success(f"[私聊][{self.private_name}] 最终规划行动: {final_action} (总耗时: {plan_duration:.3f} 秒)")
|
||||
logger.info(f"[私聊][{self.private_name}] 行动原因: {final_reason}")
|
||||
return final_action, final_reason
|
||||
|
||||
if end_success and say_bye_decision == "yes":
|
||||
# 决定要告别,返回新的 'say_goodbye' 动作
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}]结束决策: yes, 准备生成告别语. 原因: {end_decision_reason}"
|
||||
)
|
||||
# 注意:这里的 reason 可以考虑拼接初始原因和结束决策原因,或者只用结束决策原因
|
||||
final_action = "say_goodbye"
|
||||
final_reason = f"决定发送告别语。决策原因: {end_decision_reason} (原结束理由: {initial_reason})"
|
||||
return final_action, final_reason
|
||||
else:
|
||||
# 决定不告别 (包括解析失败或明确说no)
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}]结束决策: no, 直接结束对话. 原因: {end_decision_reason}"
|
||||
)
|
||||
# 返回原始的 'end_conversation' 动作
|
||||
final_action = "end_conversation"
|
||||
final_reason = initial_reason # 保持原始的结束理由
|
||||
return final_action, final_reason
|
||||
# --- Helper methods for preparing prompt inputs ---
|
||||
|
||||
except Exception as end_e:
|
||||
logger.error(f"[私聊][{self.private_name}]调用结束决策LLM或处理结果时出错: {str(end_e)}")
|
||||
# 出错时,默认执行原始的结束对话
|
||||
logger.warning(f"[私聊][{self.private_name}]结束决策出错,将按原计划执行 end_conversation")
|
||||
return "end_conversation", initial_reason # 返回原始动作和原因
|
||||
|
||||
else:
|
||||
action = initial_action
|
||||
reason = initial_reason
|
||||
|
||||
# 验证action类型 (保持不变)
|
||||
valid_actions = [
|
||||
"direct_reply",
|
||||
"send_new_message",
|
||||
"wait",
|
||||
"listening",
|
||||
"rethink_goal",
|
||||
"end_conversation", # 仍然需要验证,因为可能从上面决策后返回
|
||||
"block_and_ignore",
|
||||
"say_goodbye", # 也要验证这个新动作
|
||||
]
|
||||
if action not in valid_actions:
|
||||
logger.warning(f"[私聊][{self.private_name}]LLM返回了未知的行动类型: '{action}',强制改为 wait")
|
||||
reason = f"(原始行动'{action}'无效,已强制改为wait) {reason}"
|
||||
action = "wait"
|
||||
|
||||
logger.info(f"[私聊][{self.private_name}]规划的行动: {action}")
|
||||
logger.info(f"[私聊][{self.private_name}]行动原因: {reason}")
|
||||
return action, reason
|
||||
def _get_bot_last_speak_time_info(self, observation_info: ObservationInfo) -> str:
|
||||
"""获取机器人上次发言时间提示"""
|
||||
|
||||
time_info = ""
|
||||
try:
|
||||
if not observation_info or not observation_info.bot_id:
|
||||
return ""
|
||||
bot_id_str = str(observation_info.bot_id)
|
||||
if hasattr(observation_info, "chat_history") and observation_info.chat_history:
|
||||
for msg in reversed(observation_info.chat_history):
|
||||
if not isinstance(msg, dict):
|
||||
continue
|
||||
sender_info = msg.get("user_info", {})
|
||||
sender_id = str(sender_info.get("user_id")) if isinstance(sender_info, dict) else None
|
||||
msg_time = msg.get("time")
|
||||
if sender_id == bot_id_str and msg_time:
|
||||
time_diff = time.time() - msg_time
|
||||
if time_diff < 60.0:
|
||||
time_info = f"提示:你上一条成功发送的消息是在 {time_diff:.1f} 秒前。\n"
|
||||
break
|
||||
except AttributeError as e:
|
||||
logger.warning(f"[私聊][{self.private_name}] 获取 Bot 上次发言时间时属性错误: {e}")
|
||||
except Exception as e:
|
||||
# 外层异常处理保持不变
|
||||
logger.error(f"[私聊][{self.private_name}]规划行动时调用 LLM 或处理结果出错: {str(e)}")
|
||||
return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}"
|
||||
logger.warning(f"[私聊][{self.private_name}] 获取 Bot 上次发言时间时出错: {e}")
|
||||
return time_info
|
||||
|
||||
def _get_timeout_context(self, conversation_info: ConversationInfo) -> str:
|
||||
"""获取超时提示信息"""
|
||||
|
||||
timeout_context = ""
|
||||
try:
|
||||
if hasattr(conversation_info, "goal_list") and conversation_info.goal_list:
|
||||
last_goal_item = conversation_info.goal_list[-1]
|
||||
last_goal_text = ""
|
||||
if isinstance(last_goal_item, dict):
|
||||
last_goal_text = last_goal_item.get("goal", "")
|
||||
elif isinstance(last_goal_item, str):
|
||||
last_goal_text = last_goal_item
|
||||
if (
|
||||
isinstance(last_goal_text, str)
|
||||
and "分钟," in last_goal_text
|
||||
and "思考接下来要做什么" in last_goal_text
|
||||
):
|
||||
wait_time_str = last_goal_text.split("分钟,")[0].replace("你等待了", "").strip()
|
||||
timeout_context = f"重要提示:对方已经长时间(约 {wait_time_str} 分钟)没有回复你的消息了,请基于此情况规划下一步。\n"
|
||||
logger.debug(f"[私聊][{self.private_name}] 检测到超时目标: {last_goal_text}")
|
||||
except AttributeError as e:
|
||||
logger.warning(f"[私聊][{self.private_name}] 检查超时目标时属性错误: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[私聊][{self.private_name}] 检查超时目标时出错: {e}")
|
||||
return timeout_context
|
||||
|
||||
def _build_goals_string(self, conversation_info: ConversationInfo) -> str:
|
||||
"""构建对话目标字符串"""
|
||||
|
||||
goals_str = ""
|
||||
try:
|
||||
if hasattr(conversation_info, "goal_list") and conversation_info.goal_list:
|
||||
recent_goals = conversation_info.goal_list[-3:]
|
||||
for goal_item in recent_goals:
|
||||
goal = "目标内容缺失"
|
||||
reasoning = "没有明确原因"
|
||||
if isinstance(goal_item, dict):
|
||||
goal = goal_item.get("goal", goal)
|
||||
reasoning = goal_item.get("reasoning", reasoning)
|
||||
elif isinstance(goal_item, str):
|
||||
goal = goal_item
|
||||
goal = str(goal) if goal is not None else "目标内容缺失"
|
||||
reasoning = str(reasoning) if reasoning is not None else "没有明确原因"
|
||||
goals_str += f"- 目标:{goal}\n 原因:{reasoning}\n"
|
||||
if not goals_str:
|
||||
goals_str = "- 目前没有明确对话目标,请考虑设定一个。\n"
|
||||
else:
|
||||
goals_str = "- 目前没有明确对话目标,请考虑设定一个。\n"
|
||||
except AttributeError as e:
|
||||
logger.warning(f"[私聊][{self.private_name}] 构建对话目标字符串时属性错误: {e}")
|
||||
goals_str = "- 获取对话目标时出错。\n"
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}] 构建对话目标字符串时出错: {e}")
|
||||
goals_str = "- 构建对话目标时出错。\n"
|
||||
return goals_str
|
||||
|
||||
async def _build_chat_history_text(self, observation_info: ObservationInfo) -> str:
|
||||
"""构建聊天历史记录文本 (包含未处理消息)"""
|
||||
|
||||
chat_history_text = ""
|
||||
try:
|
||||
if hasattr(observation_info, "chat_history_str") and observation_info.chat_history_str:
|
||||
chat_history_text = observation_info.chat_history_str
|
||||
elif hasattr(observation_info, "chat_history") and observation_info.chat_history:
|
||||
history_slice = observation_info.chat_history[-20:]
|
||||
chat_history_text = await build_readable_messages(
|
||||
history_slice, replace_bot_name=True, merge_messages=False, timestamp_mode="relative", read_mark=0.0
|
||||
)
|
||||
else:
|
||||
chat_history_text = "还没有聊天记录。\n"
|
||||
unread_count = getattr(observation_info, "new_messages_count", 0)
|
||||
unread_messages = getattr(observation_info, "unprocessed_messages", [])
|
||||
if unread_count > 0 and unread_messages:
|
||||
bot_qq_str = str(global_config.BOT_QQ)
|
||||
other_unread_messages = [
|
||||
msg for msg in unread_messages if msg.get("user_info", {}).get("user_id") != bot_qq_str
|
||||
]
|
||||
other_unread_count = len(other_unread_messages)
|
||||
if other_unread_count > 0:
|
||||
new_messages_str = await build_readable_messages(
|
||||
other_unread_messages,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="relative",
|
||||
read_mark=0.0,
|
||||
)
|
||||
chat_history_text += (
|
||||
f"\n--- 以下是 {other_unread_count} 条你需要处理的新消息 ---\n{new_messages_str}\n------\n"
|
||||
)
|
||||
logger.debug(f"[私聊][{self.private_name}] 向 LLM 追加了 {other_unread_count} 条未读消息。")
|
||||
except AttributeError as e:
|
||||
logger.warning(f"[私聊][{self.private_name}] 构建聊天记录文本时属性错误: {e}")
|
||||
chat_history_text = "[获取聊天记录时出错]\n"
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}] 处理聊天记录时发生未知错误: {e}")
|
||||
chat_history_text = "[处理聊天记录时出错]\n"
|
||||
return chat_history_text
|
||||
|
||||
def _build_action_history_context(self, conversation_info: ConversationInfo) -> Tuple[str, str]:
|
||||
"""构建行动历史概要和上一次行动详细情况"""
|
||||
|
||||
action_history_summary = "你最近执行的行动历史:\n"
|
||||
last_action_context = "关于你【上一次尝试】的行动:\n"
|
||||
action_history_list: List[Dict[str, Any]] = []
|
||||
try:
|
||||
if hasattr(conversation_info, "done_action") and conversation_info.done_action:
|
||||
action_history_list = conversation_info.done_action[-5:]
|
||||
except AttributeError as e:
|
||||
logger.warning(f"[私聊][{self.private_name}] 获取行动历史时属性错误: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}] 访问行动历史时出错: {e}")
|
||||
if not action_history_list:
|
||||
action_history_summary += "- 还没有执行过行动。\n"
|
||||
last_action_context += "- 这是你规划的第一个行动。\n"
|
||||
else:
|
||||
for i, action_data in enumerate(action_history_list):
|
||||
if not isinstance(action_data, dict):
|
||||
logger.warning(f"[私聊][{self.private_name}] 行动历史记录格式错误,跳过: {action_data}")
|
||||
continue
|
||||
action_type = action_data.get("action", "未知动作")
|
||||
plan_reason = action_data.get("plan_reason", "未知规划原因")
|
||||
status = action_data.get("status", "未知状态")
|
||||
final_reason = action_data.get("final_reason", "")
|
||||
action_time = action_data.get("time", "未知时间")
|
||||
reason_text = f", 最终原因: “{final_reason}”" if final_reason else ""
|
||||
summary_line = f"- 时间:{action_time}, 尝试:'{action_type}', 状态:{status}{reason_text}"
|
||||
action_history_summary += summary_line + "\n"
|
||||
if i == len(action_history_list) - 1:
|
||||
last_action_context += f"- 上次【规划】的行动是: '{action_type}'\n"
|
||||
last_action_context += f"- 当时规划的【原因】是: {plan_reason}\n"
|
||||
if status == "done":
|
||||
last_action_context += "- 该行动已【成功执行】。\n"
|
||||
elif status == "recall" or status == "error" or status.startswith("cancelled"):
|
||||
last_action_context += "- 但该行动最终【未能成功执行/被取消/出错】。\n"
|
||||
if final_reason:
|
||||
last_action_context += f"- 【重要】失败/取消/错误原因是: “{final_reason}”\n"
|
||||
else:
|
||||
last_action_context += "- 【重要】失败/取消/错误原因未明确记录。\n"
|
||||
elif status == "start":
|
||||
last_action_context += "- 该行动【正在执行中】或【未完成】。\n"
|
||||
else:
|
||||
last_action_context += f"- 该行动当前状态未知: {status}\n"
|
||||
return action_history_summary, last_action_context
|
||||
|
||||
# --- Helper method for handling end_conversation decision ---
|
||||
|
||||
async def _handle_end_conversation_decision(
|
||||
self, persona_text: str, chat_history_text: str, initial_reason: str
|
||||
) -> Tuple[str, str]:
|
||||
"""处理结束对话前的告别决策"""
|
||||
logger.info(f"[私聊][{self.private_name}] 初步规划结束对话,进入告别决策...")
|
||||
end_decision_prompt = PROMPT_END_DECISION.format(persona_text=persona_text, chat_history_text=chat_history_text)
|
||||
logger.debug(f"[私聊][{self.private_name}] 发送到LLM的结束决策提示词:\n------\n{end_decision_prompt}\n------")
|
||||
llm_start_time = time.time()
|
||||
end_content, _ = await self.llm.generate_response_async(end_decision_prompt)
|
||||
llm_duration = time.time() - llm_start_time
|
||||
logger.debug(f"[私聊][{self.private_name}] LLM (结束决策) 耗时: {llm_duration:.3f} 秒, 原始返回: {end_content}")
|
||||
end_success, end_result = get_items_from_json(
|
||||
end_content,
|
||||
self.private_name,
|
||||
"say_bye",
|
||||
"reason",
|
||||
default_values={"say_bye": "no", "reason": "结束决策LLM返回格式错误,默认不告别"},
|
||||
required_types={"say_bye": str, "reason": str},
|
||||
)
|
||||
say_bye_decision = end_result.get("say_bye", "no").lower()
|
||||
end_decision_reason = end_result.get("reason", "未提供原因")
|
||||
if end_success and say_bye_decision == "yes":
|
||||
logger.info(f"[私聊][{self.private_name}] 结束决策: yes, 准备生成告别语. 原因: {end_decision_reason}")
|
||||
final_action = "say_goodbye"
|
||||
final_reason = f"决定发送告别语 (原因: {end_decision_reason})。原结束理由: {initial_reason}"
|
||||
return final_action, final_reason
|
||||
else:
|
||||
logger.info(f"[私聊][{self.private_name}] 结束决策: no, 直接结束对话. 原因: {end_decision_reason}")
|
||||
return "end_conversation", initial_reason
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,335 @@
|
|||
from typing import TYPE_CHECKING, Optional
|
||||
import asyncio
|
||||
import time
|
||||
import random
|
||||
from src.common.logger import get_module_logger
|
||||
from ..models.utils_model import LLMRequest
|
||||
from src.config.config import global_config
|
||||
from .chat_observer import ChatObserver
|
||||
from .message_sender import DirectMessageSender
|
||||
from ..chat.chat_stream import ChatStream
|
||||
from maim_message import UserInfo
|
||||
from src.individuality.individuality import Individuality
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .conversation import Conversation
|
||||
|
||||
logger = get_module_logger("pfc_idle")
|
||||
|
||||
|
||||
class IdleConversationStarter:
|
||||
"""长时间无对话主动发起对话的组件
|
||||
|
||||
该组件会在一段时间没有对话后,自动生成一条消息发送给用户,以保持对话的活跃度。
|
||||
时间阈值会在配置的最小和最大值之间随机选择,每次发送消息后都会重置。
|
||||
"""
|
||||
|
||||
def __init__(self, stream_id: str, private_name: str):
|
||||
"""初始化空闲对话启动器
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
private_name: 私聊用户名称
|
||||
"""
|
||||
self.stream_id: str = stream_id
|
||||
self.private_name: str = private_name
|
||||
self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
|
||||
self.message_sender = DirectMessageSender(private_name)
|
||||
|
||||
# 添加异步锁,保护对共享变量的访问
|
||||
self._lock: asyncio.Lock = asyncio.Lock()
|
||||
|
||||
# LLM请求对象,用于生成主动对话内容
|
||||
self.llm = LLMRequest(
|
||||
model=global_config.llm_normal, temperature=0.8, max_tokens=500, request_type="idle_conversation_starter"
|
||||
)
|
||||
|
||||
# 个性化信息
|
||||
self.personality_info: str = Individuality.get_instance().get_prompt(x_person=2, level=3)
|
||||
|
||||
# 计算实际触发阈值(在min和max之间随机)
|
||||
self.actual_idle_threshold: int = random.randint(global_config.min_idle_time, global_config.max_idle_time)
|
||||
|
||||
# 工作状态
|
||||
self.last_message_time: float = time.time()
|
||||
self._running: bool = False
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
|
||||
def start(self) -> None:
|
||||
"""启动空闲对话检测
|
||||
|
||||
如果功能被禁用或已经在运行,则不会启动。
|
||||
"""
|
||||
# 如果功能被禁用,则不启动
|
||||
if not global_config.enable_idle_conversation:
|
||||
logger.info(f"[私聊][{self.private_name}]主动发起对话功能已禁用")
|
||||
return
|
||||
|
||||
if self._running:
|
||||
logger.debug(f"[私聊][{self.private_name}]主动发起对话功能已在运行中")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._task = asyncio.create_task(self._check_idle_loop())
|
||||
logger.info(f"[私聊][{self.private_name}]启动空闲对话检测,阈值设置为{self.actual_idle_threshold}秒")
|
||||
|
||||
def stop(self) -> None:
|
||||
"""停止空闲对话检测
|
||||
|
||||
取消当前运行的任务并重置状态。
|
||||
"""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self._running = False
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
self._task = None
|
||||
logger.info(f"[私聊][{self.private_name}]停止空闲对话检测")
|
||||
|
||||
async def update_last_message_time(self, message_time: Optional[float] = None) -> None:
|
||||
"""更新最后一条消息的时间
|
||||
|
||||
Args:
|
||||
message_time: 消息时间戳,如果为None则使用当前时间
|
||||
"""
|
||||
async with self._lock:
|
||||
self.last_message_time = message_time or time.time()
|
||||
# 重新随机化下一次触发的时间阈值
|
||||
self.actual_idle_threshold = random.randint(global_config.min_idle_time, global_config.max_idle_time)
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}]更新最后消息时间: {self.last_message_time},新阈值: {self.actual_idle_threshold}秒"
|
||||
)
|
||||
|
||||
def reload_config(self) -> None:
|
||||
"""重新加载配置
|
||||
|
||||
记录当前配置参数,用于日志输出
|
||||
"""
|
||||
try:
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}]重新加载主动对话配置: 启用={global_config.enable_idle_conversation}, 检查间隔={global_config.idle_check_interval}秒, 最短间隔={global_config.min_idle_time}秒, 最长间隔={global_config.max_idle_time}秒"
|
||||
)
|
||||
|
||||
# 重新计算实际阈值
|
||||
async def update_threshold():
|
||||
async with self._lock:
|
||||
self.actual_idle_threshold = random.randint(
|
||||
global_config.min_idle_time, global_config.max_idle_time
|
||||
)
|
||||
logger.debug(f"[私聊][{self.private_name}]更新空闲检测阈值为: {self.actual_idle_threshold}秒")
|
||||
|
||||
# 创建一个任务来异步更新阈值
|
||||
asyncio.create_task(update_threshold())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]重新加载配置时出错: {str(e)}")
|
||||
|
||||
async def _check_idle_loop(self) -> None:
|
||||
"""检查空闲状态的循环
|
||||
|
||||
定期检查是否长时间无对话,如果达到阈值则尝试主动发起对话。
|
||||
"""
|
||||
try:
|
||||
config_reload_counter = 0
|
||||
config_reload_interval = 100 # 每100次检查重新加载一次配置
|
||||
|
||||
while self._running:
|
||||
# 定期重新加载配置
|
||||
config_reload_counter += 1
|
||||
if config_reload_counter >= config_reload_interval:
|
||||
self.reload_config()
|
||||
config_reload_counter = 0
|
||||
|
||||
# 检查是否启用了主动对话功能
|
||||
if not global_config.enable_idle_conversation:
|
||||
# 如果禁用了功能,就等待一段时间后再次检查配置
|
||||
await asyncio.sleep(global_config.idle_check_interval)
|
||||
continue
|
||||
|
||||
# 使用锁保护对共享变量的读取
|
||||
current_time = time.time()
|
||||
async with self._lock:
|
||||
idle_time = current_time - self.last_message_time
|
||||
threshold = self.actual_idle_threshold
|
||||
|
||||
if idle_time >= threshold:
|
||||
logger.info(f"[私聊][{self.private_name}]检测到长时间({idle_time:.0f}秒)无对话,尝试主动发起聊天")
|
||||
await self._initiate_conversation()
|
||||
# 更新时间,避免连续触发
|
||||
await self.update_last_message_time()
|
||||
|
||||
# 等待下一次检查
|
||||
await asyncio.sleep(global_config.idle_check_interval)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"[私聊][{self.private_name}]空闲对话检测任务被取消")
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]空闲对话检测出错: {str(e)}")
|
||||
# 尝试重新启动检测循环
|
||||
if self._running:
|
||||
logger.info(f"[私聊][{self.private_name}]尝试重新启动空闲对话检测")
|
||||
self._task = asyncio.create_task(self._check_idle_loop())
|
||||
|
||||
async def _initiate_conversation(self) -> None:
|
||||
"""生成并发送主动对话内容
|
||||
|
||||
获取聊天历史记录,使用LLM生成合适的开场白,然后发送消息。
|
||||
"""
|
||||
try:
|
||||
# 获取聊天历史记录,用于生成更合适的开场白
|
||||
messages = self.chat_observer.get_cached_messages(limit=12) # 获取最近12条消息
|
||||
chat_history_text = await build_readable_messages(
|
||||
messages,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="relative",
|
||||
read_mark=0.0,
|
||||
)
|
||||
|
||||
# 构建提示词
|
||||
prompt = f"""{self.personality_info}。你的名字是{global_config.BOT_NICKNAME}。
|
||||
你正在与用户{self.private_name}进行QQ私聊,
|
||||
但已经有一段时间没有对话了。
|
||||
你想要主动发起一个友好的对话,可以说说自己在做的事情或者询问对方在做什么。
|
||||
请基于以下之前的对话历史,生成一条自然、友好、符合你个性的主动对话消息。
|
||||
这条消息应该能够引起用户的兴趣,重新开始对话。
|
||||
最近的对话历史(可能已经过去了很久):
|
||||
{chat_history_text}
|
||||
请直接输出一条消息,不要有任何额外的解释或引导文字。消息要简短自然,就像是在日常聊天中的开场白。
|
||||
消息内容尽量简短,不要超过20个字,不要添加任何表情符号。
|
||||
"""
|
||||
|
||||
# 尝试生成回复,添加超时处理
|
||||
try:
|
||||
content, _ = await asyncio.wait_for(
|
||||
self.llm.generate_response_async(prompt),
|
||||
timeout=30, # 30秒超时
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"[私聊][{self.private_name}]生成主动对话内容超时")
|
||||
return
|
||||
except Exception as llm_err:
|
||||
logger.error(f"[私聊][{self.private_name}]生成主动对话内容失败: {str(llm_err)}")
|
||||
return
|
||||
|
||||
# 清理结果
|
||||
content = content.strip()
|
||||
content = content.strip("\"'")
|
||||
|
||||
if not content:
|
||||
logger.error(f"[私聊][{self.private_name}]生成的主动对话内容为空")
|
||||
return
|
||||
|
||||
# 统一错误处理,从这里开始所有操作都在同一个try-except块中
|
||||
logger.debug(f"[私聊][{self.private_name}]成功生成主动对话内容: {content},准备发送")
|
||||
|
||||
from .pfc_manager import PFCManager
|
||||
|
||||
# 获取当前实例
|
||||
pfc_manager = PFCManager.get_instance()
|
||||
|
||||
# 结束当前对话实例(如果存在)
|
||||
current_conversation = await pfc_manager.get_conversation(self.stream_id)
|
||||
if current_conversation:
|
||||
logger.info(f"[私聊][{self.private_name}]结束当前对话实例,准备创建新实例")
|
||||
try:
|
||||
await current_conversation.stop()
|
||||
await pfc_manager.remove_conversation(self.stream_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"[私聊][{self.private_name}]结束当前对话实例时出错: {str(e)},继续创建新实例")
|
||||
|
||||
# 创建新的对话实例
|
||||
logger.info(f"[私聊][{self.private_name}]创建新的对话实例以发送主动消息")
|
||||
new_conversation = None
|
||||
try:
|
||||
new_conversation = await pfc_manager.get_or_create_conversation(self.stream_id, self.private_name)
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]创建新对话实例失败: {str(e)}")
|
||||
return
|
||||
|
||||
# 确保新对话实例已初始化完成
|
||||
chat_stream = await self._get_chat_stream(new_conversation)
|
||||
if not chat_stream:
|
||||
logger.error(f"[私聊][{self.private_name}]无法获取有效的聊天流,取消发送主动消息")
|
||||
return
|
||||
|
||||
# 发送消息
|
||||
try:
|
||||
await self.message_sender.send_message(chat_stream=chat_stream, content=content, reply_to_message=None)
|
||||
|
||||
# 更新空闲会话启动器的最后消息时间
|
||||
await self.update_last_message_time()
|
||||
|
||||
# 如果新对话实例有一个聊天观察者,请触发更新
|
||||
if new_conversation and hasattr(new_conversation, "chat_observer"):
|
||||
logger.info(f"[私聊][{self.private_name}]触发聊天观察者更新")
|
||||
try:
|
||||
new_conversation.chat_observer.trigger_update()
|
||||
except Exception as e:
|
||||
logger.warning(f"[私聊][{self.private_name}]触发聊天观察者更新失败: {str(e)}")
|
||||
|
||||
logger.success(f"[私聊][{self.private_name}]成功主动发起对话: {content}")
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]发送主动对话消息失败: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
# 顶级异常处理,确保任何未捕获的异常都不会导致整个进程崩溃
|
||||
logger.error(f"[私聊][{self.private_name}]主动发起对话过程中发生未预期的错误: {str(e)}")
|
||||
|
||||
async def _get_chat_stream(self, conversation: Optional["Conversation"] = None) -> Optional[ChatStream]:
|
||||
"""获取可用的聊天流
|
||||
|
||||
尝试多种方式获取聊天流:
|
||||
1. 从传入的对话实例中获取
|
||||
2. 从全局聊天管理器中获取
|
||||
3. 创建一个新的聊天流
|
||||
|
||||
Args:
|
||||
conversation: 对话实例,可以为None
|
||||
|
||||
Returns:
|
||||
Optional[ChatStream]: 如果成功获取则返回聊天流,否则返回None
|
||||
"""
|
||||
chat_stream = None
|
||||
|
||||
# 1. 尝试从对话实例获取
|
||||
if conversation and hasattr(conversation, "should_continue"):
|
||||
# 等待一小段时间,确保初始化完成
|
||||
retry_count = 0
|
||||
max_retries = 10
|
||||
while not conversation.should_continue and retry_count < max_retries:
|
||||
await asyncio.sleep(0.5)
|
||||
retry_count += 1
|
||||
logger.debug(f"[私聊][{self.private_name}]等待新对话实例初始化完成: 尝试 {retry_count}/{max_retries}")
|
||||
|
||||
if not conversation.should_continue:
|
||||
logger.warning(f"[私聊][{self.private_name}]新对话实例初始化可能未完成,但仍将尝试获取聊天流")
|
||||
|
||||
# 尝试使用对话实例的聊天流
|
||||
if hasattr(conversation, "chat_stream") and conversation.chat_stream:
|
||||
logger.info(f"[私聊][{self.private_name}]使用新对话实例的聊天流")
|
||||
return conversation.chat_stream
|
||||
|
||||
# 2. 尝试从聊天管理器获取
|
||||
from src.plugins.chat.chat_stream import chat_manager
|
||||
|
||||
try:
|
||||
logger.info(f"[私聊][{self.private_name}]尝试从chat_manager获取聊天流")
|
||||
chat_stream = chat_manager.get_stream(self.stream_id)
|
||||
if chat_stream:
|
||||
return chat_stream
|
||||
except Exception as e:
|
||||
logger.warning(f"[私聊][{self.private_name}]从chat_manager获取聊天流失败: {str(e)}")
|
||||
|
||||
# 3. 创建新的聊天流
|
||||
try:
|
||||
logger.warning(f"[私聊][{self.private_name}]无法获取现有聊天流,创建新的聊天流")
|
||||
# 创建用户信息对象
|
||||
user_info = UserInfo(user_id=global_config.BOT_QQ, user_nickname=global_config.BOT_NICKNAME, platform="qq")
|
||||
# 创建聊天流
|
||||
return ChatStream(self.stream_id, "qq", user_info)
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]创建新聊天流失败: {str(e)}")
|
||||
return None
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# File: observation_info.py
|
||||
from typing import List, Optional, Dict, Any, Set
|
||||
from maim_message import UserInfo
|
||||
import time
|
||||
import traceback
|
||||
from typing import List, Optional, Dict, Any, Set
|
||||
|
||||
from maim_message import UserInfo
|
||||
from src.common.logger import get_module_logger
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
|
||||
# 确保导入路径正确
|
||||
from .chat_observer import ChatObserver
|
||||
from .chat_states import NotificationHandler, NotificationType, Notification
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
import traceback # 导入 traceback 用于调试
|
||||
|
||||
logger = get_module_logger("observation_info")
|
||||
|
||||
|
|
@ -16,324 +17,286 @@ class ObservationInfoHandler(NotificationHandler):
|
|||
"""ObservationInfo的通知处理器"""
|
||||
|
||||
def __init__(self, observation_info: "ObservationInfo", private_name: str):
|
||||
"""初始化处理器
|
||||
|
||||
Args:
|
||||
observation_info: 要更新的ObservationInfo实例
|
||||
private_name: 私聊对象的名称,用于日志记录
|
||||
"""
|
||||
"""初始化处理器"""
|
||||
self.observation_info = observation_info
|
||||
# 将 private_name 存储在 handler 实例中
|
||||
self.private_name = private_name
|
||||
|
||||
async def handle_notification(self, notification: Notification): # 添加类型提示
|
||||
# 获取通知类型和数据
|
||||
async def handle_notification(self, notification: Notification):
|
||||
"""处理来自 ChatObserver 的通知"""
|
||||
notification_type = notification.type
|
||||
data = notification.data
|
||||
timestamp = notification.timestamp # 获取通知时间戳
|
||||
|
||||
try: # 添加错误处理块
|
||||
try:
|
||||
if notification_type == NotificationType.NEW_MESSAGE:
|
||||
# 处理新消息通知
|
||||
# logger.debug(f"[私聊][{self.private_name}]收到新消息通知data: {data}") # 可以在需要时取消注释
|
||||
message_id = data.get("message_id")
|
||||
processed_plain_text = data.get("processed_plain_text")
|
||||
detailed_plain_text = data.get("detailed_plain_text")
|
||||
user_info_dict = data.get("user_info") # 先获取字典
|
||||
time_value = data.get("time")
|
||||
message_dict = data # data 本身就是消息字典
|
||||
if not isinstance(message_dict, dict):
|
||||
logger.warning(f"[私聊][{self.private_name}] 收到的 NEW_MESSAGE 数据不是字典: {data}")
|
||||
return
|
||||
|
||||
# 确保 user_info 是字典类型再创建 UserInfo 对象
|
||||
user_info = None
|
||||
# 解析 UserInfo
|
||||
user_info_dict = message_dict.get("user_info")
|
||||
user_info: Optional[UserInfo] = None
|
||||
if isinstance(user_info_dict, dict):
|
||||
try:
|
||||
user_info = UserInfo.from_dict(user_info_dict)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[私聊][{self.private_name}]从字典创建 UserInfo 时出错: {e}, 字典内容: {user_info_dict}"
|
||||
f"[私聊][{self.private_name}] 从字典创建 UserInfo 时出错: {e}, dict: {user_info_dict}"
|
||||
)
|
||||
# 可以选择在这里返回或记录错误,避免后续代码出错
|
||||
return
|
||||
elif user_info_dict is not None:
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}]收到的 user_info 不是预期的字典类型: {type(user_info_dict)}"
|
||||
f"[私聊][{self.private_name}] 收到的 user_info 不是预期的字典类型: {type(user_info_dict)}"
|
||||
)
|
||||
# 根据需要处理非字典情况,这里暂时返回
|
||||
return
|
||||
|
||||
message = {
|
||||
"message_id": message_id,
|
||||
"processed_plain_text": processed_plain_text,
|
||||
"detailed_plain_text": detailed_plain_text,
|
||||
"user_info": user_info_dict, # 存储原始字典或 UserInfo 对象,取决于你的 update_from_message 如何处理
|
||||
"time": time_value,
|
||||
}
|
||||
# 传递 UserInfo 对象(如果成功创建)或原始字典
|
||||
await self.observation_info.update_from_message(message, user_info) # 修改:传递 user_info 对象
|
||||
# 更新 ObservationInfo
|
||||
await self.observation_info.update_from_message(message_dict, user_info)
|
||||
|
||||
elif notification_type == NotificationType.COLD_CHAT:
|
||||
# 处理冷场通知
|
||||
is_cold = data.get("is_cold", False)
|
||||
await self.observation_info.update_cold_chat_status(is_cold, time.time()) # 修改:改为 await 调用
|
||||
|
||||
elif notification_type == NotificationType.ACTIVE_CHAT:
|
||||
# 处理活跃通知 (通常由 COLD_CHAT 的反向状态处理)
|
||||
is_active = data.get("is_active", False)
|
||||
self.observation_info.is_cold = not is_active
|
||||
|
||||
elif notification_type == NotificationType.BOT_SPEAKING:
|
||||
# 处理机器人说话通知 (按需实现)
|
||||
self.observation_info.is_typing = False
|
||||
self.observation_info.last_bot_speak_time = time.time()
|
||||
|
||||
elif notification_type == NotificationType.USER_SPEAKING:
|
||||
# 处理用户说话通知
|
||||
self.observation_info.is_typing = False
|
||||
self.observation_info.last_user_speak_time = time.time()
|
||||
await self.observation_info.update_cold_chat_status(is_cold, timestamp) # 使用通知时间戳
|
||||
|
||||
elif notification_type == NotificationType.MESSAGE_DELETED:
|
||||
# 处理消息删除通知
|
||||
message_id = data.get("message_id")
|
||||
# 从 unprocessed_messages 中移除被删除的消息
|
||||
original_count = len(self.observation_info.unprocessed_messages)
|
||||
self.observation_info.unprocessed_messages = [
|
||||
msg for msg in self.observation_info.unprocessed_messages if msg.get("message_id") != message_id
|
||||
]
|
||||
# --- [修改点 11] 更新 new_messages_count ---
|
||||
self.observation_info.new_messages_count = len(self.observation_info.unprocessed_messages)
|
||||
if self.observation_info.new_messages_count < original_count:
|
||||
logger.info(f"[私聊][{self.private_name}]移除了未处理的消息 (ID: {message_id}), 当前未处理数: {self.observation_info.new_messages_count}")
|
||||
message_id_to_delete = data.get("message_id")
|
||||
if message_id_to_delete:
|
||||
await self.observation_info.remove_unprocessed_message(message_id_to_delete)
|
||||
else:
|
||||
logger.warning(f"[私聊][{self.private_name}] 收到无效的消息删除通知,缺少 message_id: {data}")
|
||||
|
||||
# --- 可以根据需要处理其他通知类型 ---
|
||||
elif notification_type == NotificationType.ACTIVE_CHAT:
|
||||
is_active = data.get("is_active", False)
|
||||
# 通常由 COLD_CHAT 的反向状态处理,但也可以在这里显式处理
|
||||
await self.observation_info.update_cold_chat_status(not is_active, timestamp)
|
||||
|
||||
elif notification_type == NotificationType.BOT_SPEAKING:
|
||||
# 机器人开始说话 (例如,如果需要显示"正在输入...")
|
||||
# self.observation_info.is_typing = True
|
||||
pass # 暂时不处理
|
||||
|
||||
elif notification_type == NotificationType.USER_SPEAKING:
|
||||
# 用户开始说话
|
||||
# self.observation_info.is_typing = True
|
||||
pass # 暂时不处理
|
||||
|
||||
elif notification_type == NotificationType.USER_JOINED:
|
||||
# 处理用户加入通知 (如果适用私聊场景)
|
||||
user_id = data.get("user_id")
|
||||
if user_id:
|
||||
self.observation_info.active_users.add(str(user_id)) # 确保是字符串
|
||||
self.observation_info.active_users.add(str(user_id))
|
||||
self.observation_info.update_changed()
|
||||
|
||||
elif notification_type == NotificationType.USER_LEFT:
|
||||
# 处理用户离开通知 (如果适用私聊场景)
|
||||
user_id = data.get("user_id")
|
||||
if user_id:
|
||||
self.observation_info.active_users.discard(str(user_id)) # 确保是字符串
|
||||
self.observation_info.active_users.discard(str(user_id))
|
||||
self.observation_info.update_changed()
|
||||
|
||||
elif notification_type == NotificationType.ERROR:
|
||||
# 处理错误通知
|
||||
error_msg = data.get("error", "未提供错误信息")
|
||||
logger.error(f"[私聊][{self.private_name}]收到错误通知: {error_msg}")
|
||||
logger.error(f"[私聊][{self.private_name}] 收到错误通知: {error_msg}")
|
||||
# 可以在这里触发一些错误处理逻辑
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]处理通知时发生错误: {e}")
|
||||
logger.error(traceback.format_exc()) # 打印详细堆栈信息
|
||||
logger.error(f"[私聊][{self.private_name}] 处理通知时发生错误 (类型: {notification_type.name}): {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
# @dataclass <-- 这个,不需要了(递黄瓜)
|
||||
class ObservationInfo:
|
||||
"""决策信息类,用于收集和管理来自chat_observer的通知信息 (手动实现 __init__)"""
|
||||
|
||||
# 类型提示保留,可用于文档和静态分析
|
||||
private_name: str
|
||||
chat_history: List[Dict[str, Any]]
|
||||
chat_history_str: str
|
||||
unprocessed_messages: List[Dict[str, Any]]
|
||||
active_users: Set[str]
|
||||
last_bot_speak_time: Optional[float]
|
||||
last_user_speak_time: Optional[float]
|
||||
last_message_time: Optional[float]
|
||||
last_message_id: Optional[str]
|
||||
last_message_content: str
|
||||
last_message_sender: Optional[str]
|
||||
bot_id: Optional[str]
|
||||
chat_history_count: int
|
||||
new_messages_count: int
|
||||
cold_chat_start_time: Optional[float]
|
||||
cold_chat_duration: float
|
||||
is_typing: bool
|
||||
is_cold_chat: bool
|
||||
changed: bool
|
||||
chat_observer: Optional[ChatObserver]
|
||||
handler: Optional[ObservationInfoHandler]
|
||||
"""决策信息类,用于收集和管理来自chat_observer的通知信息"""
|
||||
|
||||
def __init__(self, private_name: str):
|
||||
"""
|
||||
手动初始化 ObservationInfo 的所有实例变量。
|
||||
"""
|
||||
|
||||
# 接收的参数
|
||||
"""初始化 ObservationInfo"""
|
||||
self.private_name: str = private_name
|
||||
|
||||
# data_list
|
||||
self.chat_history: List[Dict[str, Any]] = []
|
||||
self.chat_history_str: str = ""
|
||||
self.unprocessed_messages: List[Dict[str, Any]] = []
|
||||
self.active_users: Set[str] = set()
|
||||
# 聊天记录相关
|
||||
self.chat_history: List[Dict[str, Any]] = [] # 存储已处理的消息历史
|
||||
self.chat_history_str: str = "还没有聊天记录。" # 用于生成 Prompt 的历史记录字符串
|
||||
self.chat_history_count: int = 0
|
||||
|
||||
# data
|
||||
# 未处理消息相关 (核心修改点)
|
||||
self.unprocessed_messages: List[Dict[str, Any]] = [] # 存储尚未被机器人回复的消息
|
||||
self.new_messages_count: int = 0 # unprocessed_messages 的数量
|
||||
|
||||
# 状态信息
|
||||
self.active_users: Set[str] = set() # 当前活跃用户 (私聊场景可能只有对方)
|
||||
self.last_bot_speak_time: Optional[float] = None
|
||||
self.last_user_speak_time: Optional[float] = None
|
||||
self.last_message_time: Optional[float] = None
|
||||
self.last_user_speak_time: Optional[float] = None # 指对方用户的发言时间
|
||||
self.last_message_time: Optional[float] = None # 指所有消息(包括自己)的最新时间
|
||||
self.last_message_id: Optional[str] = None
|
||||
self.last_message_content: str = ""
|
||||
self.last_message_sender: Optional[str] = None
|
||||
self.bot_id: Optional[str] = None # 需要在某个地方设置 bot_id,例如从 global_config 获取
|
||||
self.chat_history_count: int = 0
|
||||
self.new_messages_count: int = 0
|
||||
self.last_message_sender: Optional[str] = None # user_id of the last message sender
|
||||
self.bot_id: Optional[str] = None # 机器人自己的 ID
|
||||
|
||||
# 冷场状态
|
||||
self.cold_chat_start_time: Optional[float] = None
|
||||
self.cold_chat_duration: float = 0.0
|
||||
self.is_cold_chat: bool = False # 当前是否处于冷场状态
|
||||
|
||||
# state
|
||||
self.is_typing: bool = False
|
||||
self.is_cold_chat: bool = False
|
||||
self.changed: bool = False
|
||||
# 其他状态
|
||||
self.is_typing: bool = False # 是否正在输入 (未来可能用到)
|
||||
self.changed: bool = False # 状态是否有变化 (用于优化)
|
||||
|
||||
# 关联对象
|
||||
self.chat_observer: Optional[ChatObserver] = None
|
||||
self.handler: Optional[ObservationInfoHandler] = ObservationInfoHandler(self, self.private_name)
|
||||
|
||||
self.handler: ObservationInfoHandler = ObservationInfoHandler(self, self.private_name)
|
||||
# 初始化 bot_id
|
||||
try:
|
||||
from ...config.config import global_config
|
||||
|
||||
# --- 初始化 bot_id ---
|
||||
from ...config.config import global_config # 移动到 __init__ 内部以避免循环导入问题
|
||||
self.bot_id = str(global_config.BOT_QQ) if global_config.BOT_QQ else None
|
||||
self.bot_id = str(global_config.BOT_QQ) if global_config.BOT_QQ else None
|
||||
if not self.bot_id:
|
||||
logger.error(f"[私聊][{self.private_name}] 未能从配置中获取 BOT_QQ ID!")
|
||||
except ImportError:
|
||||
logger.error(f"[私聊][{self.private_name}] 无法导入 global_config 获取 BOT_QQ ID!")
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}] 获取 BOT_QQ ID 时出错: {e}")
|
||||
|
||||
def bind_to_chat_observer(self, chat_observer: ChatObserver):
|
||||
"""绑定到指定的chat_observer (保持不变)"""
|
||||
"""绑定到指定的 ChatObserver 并注册通知处理器"""
|
||||
if self.chat_observer:
|
||||
logger.warning(f"[私聊][{self.private_name}]尝试重复绑定 ChatObserver")
|
||||
logger.warning(f"[私聊][{self.private_name}] 尝试重复绑定 ChatObserver")
|
||||
return
|
||||
if not self.handler:
|
||||
logger.error(f"[私聊][{self.private_name}] ObservationInfoHandler 未初始化,无法绑定!")
|
||||
return
|
||||
|
||||
self.chat_observer = chat_observer
|
||||
try:
|
||||
if not self.handler:
|
||||
logger.error(f"[私聊][{self.private_name}] 尝试绑定时 handler 未初始化!")
|
||||
self.chat_observer = None
|
||||
return
|
||||
# 注册需要处理的通知类型
|
||||
notification_manager = self.chat_observer.notification_manager
|
||||
notification_manager.register_handler("observation_info", NotificationType.NEW_MESSAGE, self.handler)
|
||||
notification_manager.register_handler("observation_info", NotificationType.COLD_CHAT, self.handler)
|
||||
notification_manager.register_handler("observation_info", NotificationType.MESSAGE_DELETED, self.handler)
|
||||
# 根据需要注册更多类型...
|
||||
# notification_manager.register_handler("observation_info", NotificationType.ACTIVE_CHAT, self.handler)
|
||||
# notification_manager.register_handler("observation_info", NotificationType.USER_JOINED, self.handler)
|
||||
# notification_manager.register_handler("observation_info", NotificationType.USER_LEFT, self.handler)
|
||||
# notification_manager.register_handler("observation_info", NotificationType.ERROR, self.handler)
|
||||
|
||||
self.chat_observer.notification_manager.register_handler(
|
||||
target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler
|
||||
)
|
||||
self.chat_observer.notification_manager.register_handler(
|
||||
target="observation_info", notification_type=NotificationType.COLD_CHAT, handler=self.handler
|
||||
)
|
||||
# --- [修改点 12] 注册 MESSAGE_DELETED ---
|
||||
self.chat_observer.notification_manager.register_handler(
|
||||
target="observation_info", notification_type=NotificationType.MESSAGE_DELETED, handler=self.handler
|
||||
)
|
||||
logger.info(f"[私聊][{self.private_name}]成功绑定到 ChatObserver")
|
||||
logger.info(f"[私聊][{self.private_name}] ObservationInfo 成功绑定到 ChatObserver")
|
||||
except AttributeError:
|
||||
logger.error(f"[私聊][{self.private_name}] 绑定的 ChatObserver 对象缺少 notification_manager 属性!")
|
||||
self.chat_observer = None # 绑定失败
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]绑定到 ChatObserver 时出错: {e}")
|
||||
self.chat_observer = None
|
||||
logger.error(f"[私聊][{self.private_name}] 绑定到 ChatObserver 时出错: {e}")
|
||||
self.chat_observer = None # 绑定失败
|
||||
|
||||
def unbind_from_chat_observer(self):
|
||||
"""解除与chat_observer的绑定 (保持不变)"""
|
||||
if (
|
||||
self.chat_observer and hasattr(self.chat_observer, "notification_manager") and self.handler
|
||||
):
|
||||
"""解除与 ChatObserver 的绑定"""
|
||||
if self.chat_observer and hasattr(self.chat_observer, "notification_manager") and self.handler:
|
||||
try:
|
||||
self.chat_observer.notification_manager.unregister_handler(
|
||||
target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler
|
||||
notification_manager = self.chat_observer.notification_manager
|
||||
notification_manager.unregister_handler("observation_info", NotificationType.NEW_MESSAGE, self.handler)
|
||||
notification_manager.unregister_handler("observation_info", NotificationType.COLD_CHAT, self.handler)
|
||||
notification_manager.unregister_handler(
|
||||
"observation_info", NotificationType.MESSAGE_DELETED, self.handler
|
||||
)
|
||||
self.chat_observer.notification_manager.unregister_handler(
|
||||
target="observation_info", notification_type=NotificationType.COLD_CHAT, handler=self.handler
|
||||
)
|
||||
# --- [修改点 13] 注销 MESSAGE_DELETED ---
|
||||
self.chat_observer.notification_manager.unregister_handler(
|
||||
target="observation_info", notification_type=NotificationType.MESSAGE_DELETED, handler=self.handler
|
||||
)
|
||||
logger.info(f"[私聊][{self.private_name}]成功从 ChatObserver 解绑")
|
||||
# ... 注销其他已注册的类型 ...
|
||||
|
||||
logger.info(f"[私聊][{self.private_name}] ObservationInfo 成功从 ChatObserver 解绑")
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]从 ChatObserver 解绑时出错: {e}")
|
||||
logger.error(f"[私聊][{self.private_name}] 从 ChatObserver 解绑时出错: {e}")
|
||||
finally:
|
||||
self.chat_observer = None
|
||||
self.chat_observer = None # 无论成功与否都清除引用
|
||||
else:
|
||||
logger.warning(f"[私聊][{self.private_name}]尝试解绑时 ChatObserver 不存在、无效或 handler 未设置")
|
||||
logger.warning(f"[私聊][{self.private_name}] 尝试解绑时 ChatObserver 无效或 handler 未设置")
|
||||
|
||||
async def update_from_message(self, message: Dict[str, Any], user_info: Optional[UserInfo]):
|
||||
"""从消息更新信息 (保持不变)"""
|
||||
"""根据收到的新消息更新 ObservationInfo 的状态"""
|
||||
message_time = message.get("time")
|
||||
message_id = message.get("message_id")
|
||||
processed_text = message.get("processed_plain_text", "")
|
||||
sender_id_str: Optional[str] = str(user_info.user_id) if user_info else None
|
||||
|
||||
if message_time and message_time > (self.last_message_time or 0):
|
||||
if not message_time or not message_id:
|
||||
logger.warning(f"[私聊][{self.private_name}] 收到的消息缺少 time 或 message_id: {message}")
|
||||
return
|
||||
|
||||
# 更新最后消息时间(所有消息)
|
||||
if message_time > (self.last_message_time or 0):
|
||||
self.last_message_time = message_time
|
||||
self.last_message_id = message_id
|
||||
self.last_message_content = processed_text
|
||||
self.is_cold_chat = False
|
||||
self.cold_chat_start_time = None
|
||||
self.cold_chat_duration = 0.0
|
||||
self.last_message_sender = sender_id_str
|
||||
|
||||
if user_info:
|
||||
sender_id = str(user_info.user_id)
|
||||
self.last_message_sender = sender_id
|
||||
if sender_id == self.bot_id:
|
||||
self.last_bot_speak_time = message_time
|
||||
else:
|
||||
self.last_user_speak_time = message_time
|
||||
self.active_users.add(sender_id)
|
||||
# 更新说话者特定时间
|
||||
if sender_id_str:
|
||||
if sender_id_str == self.bot_id:
|
||||
self.last_bot_speak_time = message_time
|
||||
else:
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}]处理消息更新时缺少有效的 UserInfo 对象, message_id: {message_id}"
|
||||
)
|
||||
self.last_message_sender = None
|
||||
|
||||
# --- [修改点 14] 添加到未处理列表,并更新计数 ---
|
||||
# 检查消息是否已存在于未处理列表中,避免重复添加
|
||||
if not any(msg.get("message_id") == message_id for msg in self.unprocessed_messages):
|
||||
self.unprocessed_messages.append(message)
|
||||
self.new_messages_count = len(self.unprocessed_messages)
|
||||
logger.debug(f"[私聊][{self.private_name}]添加新未处理消息 ID: {message_id}, 当前未处理数: {self.new_messages_count}")
|
||||
self.update_changed()
|
||||
else:
|
||||
logger.warning(f"[私聊][{self.private_name}]尝试重复添加未处理消息 ID: {message_id}")
|
||||
|
||||
self.last_user_speak_time = message_time
|
||||
self.active_users.add(sender_id_str) # 添加到活跃用户
|
||||
else:
|
||||
pass
|
||||
logger.warning(f"[私聊][{self.private_name}] 处理消息更新时缺少有效的 UserInfo, message_id: {message_id}")
|
||||
|
||||
# 更新冷场状态
|
||||
self.is_cold_chat = False
|
||||
self.cold_chat_start_time = None
|
||||
self.cold_chat_duration = 0.0
|
||||
|
||||
def update_changed(self):
|
||||
"""标记状态已改变,并重置标记 (保持不变)"""
|
||||
self.changed = True
|
||||
# --- [核心修改] 将新消息添加到未处理列表 ---
|
||||
# 检查消息是否已存在于未处理列表中,避免重复添加
|
||||
if not any(msg.get("message_id") == message_id for msg in self.unprocessed_messages):
|
||||
# 创建消息的副本以避免修改原始数据(如果需要)
|
||||
self.unprocessed_messages.append(message.copy())
|
||||
self.new_messages_count = len(self.unprocessed_messages)
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}] 添加新未处理消息 ID: {message_id}, 发送者: {sender_id_str}, 当前未处理数: {self.new_messages_count}"
|
||||
)
|
||||
self.update_changed()
|
||||
else:
|
||||
logger.warning(f"[私聊][{self.private_name}] 尝试重复添加未处理消息 ID: {message_id}")
|
||||
|
||||
async def remove_unprocessed_message(self, message_id_to_delete: str):
|
||||
"""从 unprocessed_messages 列表中移除指定 ID 的消息"""
|
||||
original_count = len(self.unprocessed_messages)
|
||||
self.unprocessed_messages = [
|
||||
msg for msg in self.unprocessed_messages if msg.get("message_id") != message_id_to_delete
|
||||
]
|
||||
new_count = len(self.unprocessed_messages)
|
||||
|
||||
if new_count < original_count:
|
||||
self.new_messages_count = new_count
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}] 移除了未处理的消息 (ID: {message_id_to_delete}), 当前未处理数: {self.new_messages_count}"
|
||||
)
|
||||
self.update_changed()
|
||||
else:
|
||||
logger.warning(f"[私聊][{self.private_name}] 尝试移除不存在的未处理消息 ID: {message_id_to_delete}")
|
||||
|
||||
async def update_cold_chat_status(self, is_cold: bool, current_time: float):
|
||||
"""更新冷场状态 (保持不变)"""
|
||||
"""更新冷场状态"""
|
||||
if is_cold != self.is_cold_chat:
|
||||
self.is_cold_chat = is_cold
|
||||
if is_cold:
|
||||
self.cold_chat_start_time = (
|
||||
self.last_message_time or current_time
|
||||
)
|
||||
logger.info(f"[私聊][{self.private_name}]进入冷场状态,开始时间: {self.cold_chat_start_time}")
|
||||
# 冷场开始时间应基于最后一条消息的时间
|
||||
self.cold_chat_start_time = self.last_message_time or current_time
|
||||
logger.info(f"[私聊][{self.private_name}] 进入冷场状态,开始时间: {self.cold_chat_start_time:.2f}")
|
||||
else:
|
||||
if self.cold_chat_start_time:
|
||||
self.cold_chat_duration = current_time - self.cold_chat_start_time
|
||||
logger.info(f"[私聊][{self.private_name}]结束冷场状态,持续时间: {self.cold_chat_duration:.2f} 秒")
|
||||
self.cold_chat_start_time = None
|
||||
logger.info(f"[私聊][{self.private_name}] 结束冷场状态,持续时间: {self.cold_chat_duration:.2f} 秒")
|
||||
self.cold_chat_start_time = None # 结束冷场,重置开始时间
|
||||
self.update_changed()
|
||||
|
||||
# 持续更新冷场时长
|
||||
if self.is_cold_chat and self.cold_chat_start_time:
|
||||
self.cold_chat_duration = current_time - self.cold_chat_start_time
|
||||
|
||||
def get_active_duration(self) -> float:
|
||||
"""获取当前活跃时长 (保持不变)"""
|
||||
if not self.last_message_time:
|
||||
return 0.0
|
||||
return time.time() - self.last_message_time
|
||||
|
||||
def get_user_response_time(self) -> Optional[float]:
|
||||
"""获取用户最后响应时间 (保持不变)"""
|
||||
if not self.last_user_speak_time:
|
||||
return None
|
||||
return time.time() - self.last_user_speak_time
|
||||
|
||||
def get_bot_response_time(self) -> Optional[float]:
|
||||
"""获取机器人最后响应时间 (保持不变)"""
|
||||
if not self.last_bot_speak_time:
|
||||
return None
|
||||
return time.time() - self.last_bot_speak_time
|
||||
def update_changed(self):
|
||||
"""标记状态已改变"""
|
||||
self.changed = True
|
||||
# 这个标记通常在处理完改变后由外部逻辑重置为 False
|
||||
|
||||
# --- [修改点 15] 重命名并修改 clear_unprocessed_messages ---
|
||||
# async def clear_unprocessed_messages(self): <-- 旧方法注释掉或删除
|
||||
async def clear_processed_messages(self, message_ids_to_clear: Set[str]):
|
||||
"""将指定ID的未处理消息移入历史记录,并更新相关状态"""
|
||||
"""将指定 ID 的未处理消息移入历史记录,并更新相关状态"""
|
||||
if not message_ids_to_clear:
|
||||
logger.debug(f"[私聊][{self.private_name}]没有需要清理的消息 ID。")
|
||||
logger.debug(f"[私聊][{self.private_name}] 没有需要清理的消息 ID。")
|
||||
return
|
||||
|
||||
messages_to_move = []
|
||||
|
|
@ -342,36 +305,42 @@ class ObservationInfo:
|
|||
|
||||
# 分离要清理和要保留的消息
|
||||
for msg in self.unprocessed_messages:
|
||||
if msg.get("message_id") in message_ids_to_clear:
|
||||
msg_id = msg.get("message_id")
|
||||
if msg_id in message_ids_to_clear:
|
||||
messages_to_move.append(msg)
|
||||
cleared_count += 1
|
||||
else:
|
||||
remaining_messages.append(msg)
|
||||
|
||||
if not messages_to_move:
|
||||
logger.debug(f"[私聊][{self.private_name}]未找到与 ID 列表匹配的未处理消息进行清理。")
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}] 未找到与 ID 列表 {message_ids_to_clear} 匹配的未处理消息进行清理。"
|
||||
)
|
||||
return
|
||||
|
||||
logger.debug(f"[私聊][{self.private_name}]准备清理 {cleared_count} 条已处理消息...")
|
||||
logger.debug(f"[私聊][{self.private_name}] 准备清理 {cleared_count} 条已处理消息...")
|
||||
|
||||
# 将要移动的消息添加到历史记录
|
||||
max_history_len = 100
|
||||
# 将要移动的消息添加到历史记录 (按时间排序)
|
||||
messages_to_move.sort(key=lambda m: m.get("time", 0))
|
||||
self.chat_history.extend(messages_to_move)
|
||||
|
||||
# 限制历史记录长度 (可选)
|
||||
max_history_len = 100 # 例如保留最近 100 条
|
||||
if len(self.chat_history) > max_history_len:
|
||||
self.chat_history = self.chat_history[-max_history_len:]
|
||||
|
||||
# 更新历史记录字符串 (仅使用最近一部分生成)
|
||||
history_slice_for_str = self.chat_history[-20:] # 例如最近20条
|
||||
# 更新历史记录字符串 (仅使用最近一部分生成,提高效率)
|
||||
history_slice_for_str = self.chat_history[-20:] # 例如最近 20 条
|
||||
try:
|
||||
self.chat_history_str = await build_readable_messages(
|
||||
history_slice_for_str,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="relative",
|
||||
read_mark=0.0,
|
||||
read_mark=0.0, # read_mark 可能需要调整或移除
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{self.private_name}]构建聊天记录字符串时出错: {e}")
|
||||
logger.error(f"[私聊][{self.private_name}] 构建聊天记录字符串时出错: {e}")
|
||||
self.chat_history_str = "[构建聊天记录出错]"
|
||||
|
||||
# 更新未处理消息列表和计数
|
||||
|
|
@ -379,6 +348,27 @@ class ObservationInfo:
|
|||
self.new_messages_count = len(self.unprocessed_messages)
|
||||
self.chat_history_count = len(self.chat_history)
|
||||
|
||||
logger.info(f"[私聊][{self.private_name}]已清理 {cleared_count} 条消息,剩余未处理 {self.new_messages_count} 条,当前历史记录 {self.chat_history_count} 条。")
|
||||
logger.info(
|
||||
f"[私聊][{self.private_name}] 已清理 {cleared_count} 条消息 (IDs: {message_ids_to_clear}),剩余未处理 {self.new_messages_count} 条,当前历史记录 {self.chat_history_count} 条。"
|
||||
)
|
||||
|
||||
self.update_changed() # 状态改变
|
||||
self.update_changed() # 状态改变
|
||||
|
||||
# --- Helper methods (可以根据需要添加) ---
|
||||
def get_active_duration(self) -> float:
|
||||
"""获取当前活跃时长(距离最后一条消息的时间)"""
|
||||
if not self.last_message_time:
|
||||
return float("inf") # 或返回 0.0,取决于定义
|
||||
return time.time() - self.last_message_time
|
||||
|
||||
def get_user_response_time(self) -> Optional[float]:
|
||||
"""获取对方最后响应时间(距离对方最后一条消息的时间)"""
|
||||
if not self.last_user_speak_time:
|
||||
return None
|
||||
return time.time() - self.last_user_speak_time
|
||||
|
||||
def get_bot_response_time(self) -> Optional[float]:
|
||||
"""获取机器人最后响应时间(距离机器人最后一条消息的时间)"""
|
||||
if not self.last_bot_speak_time:
|
||||
return None
|
||||
return time.time() - self.last_bot_speak_time
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import time
|
||||
import asyncio # 引入 asyncio
|
||||
import traceback
|
||||
from typing import Dict, Optional
|
||||
|
||||
from src.common.logger import get_module_logger
|
||||
from .conversation import Conversation
|
||||
import traceback
|
||||
|
||||
logger = get_module_logger("pfc_manager")
|
||||
|
||||
|
|
@ -15,101 +17,182 @@ class PFCManager:
|
|||
|
||||
# 会话实例管理
|
||||
_instances: Dict[str, Conversation] = {}
|
||||
_initializing: Dict[str, bool] = {}
|
||||
_initializing: Dict[str, bool] = {} # 用于防止并发初始化同一个 stream_id
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "PFCManager":
|
||||
"""获取管理器单例
|
||||
|
||||
Returns:
|
||||
PFCManager: 管理器实例
|
||||
"""
|
||||
"""获取管理器单例"""
|
||||
if cls._instance is None:
|
||||
cls._instance = PFCManager()
|
||||
return cls._instance
|
||||
|
||||
async def get_or_create_conversation(self, stream_id: str, private_name: str) -> Optional[Conversation]:
|
||||
"""获取或创建对话实例
|
||||
"""获取或创建对话实例,并确保其启动"""
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
private_name: 私聊名称
|
||||
# 检查是否正在初始化 (防止并发问题)
|
||||
if self._initializing.get(stream_id, False):
|
||||
logger.debug(f"[私聊][{private_name}] 会话实例正在初始化中,请稍候: {stream_id}")
|
||||
# 可以选择等待一小段时间或直接返回 None
|
||||
await asyncio.sleep(0.5) # 短暂等待,让初始化有机会完成
|
||||
# 再次检查实例是否存在
|
||||
if stream_id in self._instances and self._instances[stream_id]._initialized:
|
||||
logger.debug(f"[私聊][{private_name}] 初始化已完成,返回现有实例: {stream_id}")
|
||||
return self._instances[stream_id]
|
||||
else:
|
||||
logger.warning(f"[私聊][{private_name}] 等待后实例仍未初始化完成或不存在。")
|
||||
return None # 避免返回未完成的实例
|
||||
|
||||
Returns:
|
||||
Optional[Conversation]: 对话实例,创建失败则返回None
|
||||
"""
|
||||
# 检查是否已经有实例
|
||||
if stream_id in self._initializing and self._initializing[stream_id]:
|
||||
logger.debug(f"[私聊][{private_name}]会话实例正在初始化中: {stream_id}")
|
||||
return None
|
||||
|
||||
if stream_id in self._instances and self._instances[stream_id].should_continue:
|
||||
logger.debug(f"[私聊][{private_name}]使用现有会话实例: {stream_id}")
|
||||
return self._instances[stream_id]
|
||||
# 检查是否已有活动实例
|
||||
if stream_id in self._instances:
|
||||
instance = self._instances[stream_id]
|
||||
# 检查忽略状态
|
||||
if (
|
||||
hasattr(instance, "ignore_until_timestamp")
|
||||
and instance.ignore_until_timestamp
|
||||
and time.time() < instance.ignore_until_timestamp
|
||||
):
|
||||
logger.debug(f"[私聊][{private_name}]会话实例当前处于忽略状态: {stream_id}")
|
||||
# 返回 None 阻止交互。或者可以返回实例但标记它被忽略了喵?
|
||||
# 还是返回 None 吧喵。
|
||||
return None
|
||||
logger.debug(f"[私聊][{private_name}] 会话实例当前处于忽略状态: {stream_id}")
|
||||
return None # 处于忽略状态,不返回实例
|
||||
|
||||
# 检查 should_continue 状态
|
||||
if instance.should_continue:
|
||||
logger.debug(f"[私聊][{private_name}]使用现有会话实例: {stream_id}")
|
||||
# 检查是否已初始化且应继续运行
|
||||
if instance._initialized and instance.should_continue:
|
||||
logger.debug(f"[私聊][{private_name}] 使用现有活动会话实例: {stream_id}")
|
||||
return instance
|
||||
# else: 实例存在但不应继续
|
||||
else:
|
||||
# 如果实例存在但未初始化或不应继续,清理旧实例
|
||||
logger.warning(f"[私聊][{private_name}] 发现无效或已停止的旧实例,清理并重新创建: {stream_id}")
|
||||
await self._cleanup_conversation(instance)
|
||||
# 从字典中移除,确保下面能创建新的
|
||||
if stream_id in self._instances:
|
||||
del self._instances[stream_id]
|
||||
if stream_id in self._initializing:
|
||||
del self._initializing[stream_id]
|
||||
|
||||
# --- 创建并初始化新实例 ---
|
||||
conversation_instance: Optional[Conversation] = None
|
||||
try:
|
||||
# 创建新实例
|
||||
logger.info(f"[私聊][{private_name}]创建新的对话实例: {stream_id}")
|
||||
self._initializing[stream_id] = True
|
||||
logger.info(f"[私聊][{private_name}] 创建新的对话实例: {stream_id}")
|
||||
self._initializing[stream_id] = True # 标记开始初始化
|
||||
|
||||
# 创建实例
|
||||
conversation_instance = Conversation(stream_id, private_name)
|
||||
self._instances[stream_id] = conversation_instance
|
||||
self._instances[stream_id] = conversation_instance # 立即存入字典
|
||||
|
||||
# 启动实例初始化
|
||||
# **启动实例初始化**
|
||||
# _initialize_conversation 会调用 conversation._initialize()
|
||||
await self._initialize_conversation(conversation_instance)
|
||||
|
||||
# --- 关键修复:在初始化成功后调用 start() ---
|
||||
if conversation_instance._initialized and conversation_instance.should_continue:
|
||||
logger.info(f"[私聊][{private_name}] 初始化成功,调用 conversation.start() 启动主循环...")
|
||||
await conversation_instance.start() # 确保调用 start 方法
|
||||
else:
|
||||
# 如果 _initialize_conversation 内部初始化失败
|
||||
logger.error(f"[私聊][{private_name}] 初始化未成功完成,无法启动实例 {stream_id}。")
|
||||
# 清理可能部分创建的实例
|
||||
await self._cleanup_conversation(conversation_instance)
|
||||
if stream_id in self._instances:
|
||||
del self._instances[stream_id]
|
||||
conversation_instance = None # 返回 None 表示失败
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{private_name}]创建会话实例失败: {stream_id}, 错误: {e}")
|
||||
return None
|
||||
logger.error(f"[私聊][{private_name}] 创建或启动会话实例时发生严重错误: {stream_id}, 错误: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
# 确保清理
|
||||
if conversation_instance:
|
||||
await self._cleanup_conversation(conversation_instance)
|
||||
if stream_id in self._instances:
|
||||
del self._instances[stream_id]
|
||||
conversation_instance = None # 返回 None
|
||||
|
||||
finally:
|
||||
# 确保初始化标记被清除
|
||||
if stream_id in self._initializing:
|
||||
self._initializing[stream_id] = False
|
||||
|
||||
return conversation_instance
|
||||
|
||||
async def _initialize_conversation(self, conversation: Conversation):
|
||||
"""初始化会话实例
|
||||
|
||||
Args:
|
||||
conversation: 要初始化的会话实例
|
||||
"""
|
||||
"""(内部方法) 初始化会话实例的核心逻辑"""
|
||||
stream_id = conversation.stream_id
|
||||
private_name = conversation.private_name
|
||||
|
||||
try:
|
||||
logger.info(f"[私聊][{private_name}]开始初始化会话实例: {stream_id}")
|
||||
# 启动初始化流程
|
||||
await conversation._initialize()
|
||||
|
||||
# 标记初始化完成
|
||||
self._initializing[stream_id] = False
|
||||
|
||||
logger.info(f"[私聊][{private_name}]会话实例 {stream_id} 初始化完成")
|
||||
logger.info(f"[私聊][{private_name}] 管理器开始调用 conversation._initialize(): {stream_id}")
|
||||
await conversation._initialize() # 调用实例自身的初始化方法
|
||||
# 注意:初始化成功与否由 conversation._initialized 和 conversation.should_continue 标志决定
|
||||
if conversation._initialized:
|
||||
logger.info(
|
||||
f"[私聊][{private_name}] conversation._initialize() 调用完成,实例标记为已初始化: {stream_id}"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[私聊][{private_name}] conversation._initialize() 调用完成,但实例未成功标记为已初始化: {stream_id}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{private_name}]管理器初始化会话实例失败: {stream_id}, 错误: {e}")
|
||||
logger.error(f"[私聊][{private_name}]{traceback.format_exc()}")
|
||||
# 清理失败的初始化
|
||||
# _initialize 内部应该处理自己的异常,但这里也捕获以防万一
|
||||
logger.error(
|
||||
f"[私聊][{private_name}] 调用 conversation._initialize() 时发生未捕获错误: {stream_id}, 错误: {e}"
|
||||
)
|
||||
logger.error(traceback.format_exc())
|
||||
# 确保实例状态反映失败
|
||||
conversation._initialized = False
|
||||
conversation.should_continue = False
|
||||
|
||||
async def _cleanup_conversation(self, conversation: Conversation):
|
||||
"""清理会话实例的资源"""
|
||||
if not conversation:
|
||||
return
|
||||
stream_id = conversation.stream_id
|
||||
private_name = conversation.private_name
|
||||
logger.info(f"[私聊][{private_name}] 开始清理会话实例资源: {stream_id}")
|
||||
try:
|
||||
# 调用 conversation 的 stop 方法来停止其内部组件
|
||||
if hasattr(conversation, "stop") and callable(conversation.stop):
|
||||
await conversation.stop() # stop 方法应处理内部组件的停止
|
||||
else:
|
||||
logger.warning(f"[私聊][{private_name}] Conversation 对象缺少 stop 方法,可能无法完全清理资源。")
|
||||
# 尝试手动停止已知组件 (作为后备)
|
||||
if hasattr(conversation, "idle_conversation_starter") and conversation.idle_conversation_starter:
|
||||
conversation.idle_conversation_starter.stop()
|
||||
if hasattr(conversation, "observation_info") and conversation.observation_info:
|
||||
conversation.observation_info.unbind_from_chat_observer()
|
||||
# ChatObserver 是单例,不在此处停止
|
||||
|
||||
logger.info(f"[私聊][{private_name}] 会话实例 {stream_id} 资源已清理")
|
||||
except Exception as e:
|
||||
logger.error(f"[私聊][{private_name}] 清理会话实例资源时失败: {stream_id}, 错误: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
async def get_conversation(self, stream_id: str) -> Optional[Conversation]:
|
||||
"""获取已存在的会话实例
|
||||
"""获取已存在的会话实例 (只读)"""
|
||||
instance = self._instances.get(stream_id)
|
||||
if instance and instance._initialized and instance.should_continue:
|
||||
# 检查忽略状态
|
||||
if (
|
||||
hasattr(instance, "ignore_until_timestamp")
|
||||
and instance.ignore_until_timestamp
|
||||
and time.time() < instance.ignore_until_timestamp
|
||||
):
|
||||
return None # 忽略期间不返回
|
||||
return instance
|
||||
return None # 不存在或无效则返回 None
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
|
||||
Returns:
|
||||
Optional[Conversation]: 会话实例,不存在则返回None
|
||||
"""
|
||||
return self._instances.get(stream_id)
|
||||
async def remove_conversation(self, stream_id: str):
|
||||
"""移除并清理会话实例"""
|
||||
if stream_id in self._instances:
|
||||
instance_to_remove = self._instances[stream_id]
|
||||
logger.info(f"[管理器] 准备移除并清理会话实例: {stream_id}")
|
||||
try:
|
||||
# 先从字典中移除引用,防止新的请求获取到正在清理的实例
|
||||
del self._instances[stream_id]
|
||||
if stream_id in self._initializing:
|
||||
del self._initializing[stream_id]
|
||||
# 清理资源
|
||||
await self._cleanup_conversation(instance_to_remove)
|
||||
logger.info(f"[管理器] 会话实例 {stream_id} 已成功移除并清理")
|
||||
except Exception as e:
|
||||
logger.error(f"[管理器] 移除或清理会话实例 {stream_id} 时失败: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
else:
|
||||
logger.warning(f"[管理器] 尝试移除不存在的会话实例: {stream_id}")
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class ConversationState(Enum):
|
|||
ENDED = "结束"
|
||||
JUDGING = "判断"
|
||||
IGNORED = "屏蔽"
|
||||
ERROR = "错误" # <--- 添加 ERROR 状态
|
||||
|
||||
|
||||
ActionType = Literal["direct_reply", "fetch_knowledge", "wait"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[inner]
|
||||
version = "1.6.1.5"
|
||||
version = "1.6.2"
|
||||
|
||||
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
|
||||
#如果你想要修改配置文件,请在修改后将version的值进行变更
|
||||
|
|
@ -70,8 +70,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 # 最多允许多少个群进行专注聊天
|
||||
allow_remove_duplicates = true # 是否开启心流去重(如果发现心流截断问题严重可尝试关闭)
|
||||
|
||||
observation_context_size = 15 # 观察到的最长上下文大小,建议15,太短太长都会导致脑袋尖尖
|
||||
|
|
@ -202,6 +202,12 @@ talk_allowed_private = [] # 可以回复消息的QQ号
|
|||
pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立
|
||||
api_polling_max_retries = 3
|
||||
|
||||
[idle_conversation]
|
||||
enable_idle_conversation = true
|
||||
idle_check_interval = 10 # 检查间隔,10分钟检查一次
|
||||
min_idle_time = 7200 # 最短无活动时间,2小时 (7200秒)
|
||||
max_idle_time = 18000 # 最长无活动时间,5小时 (18000秒)
|
||||
|
||||
#下面的模型若使用硅基流动则不需要更改,使用ds官方则改成.env自定义的宏,使用自定义模型则选择定位相似的模型自己填写
|
||||
#推理模型
|
||||
|
||||
|
|
|
|||
|
|
@ -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