You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

564 lines
24 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# -*- coding: utf-8 -*-
"""
对话管理器模块
该模块实现了对话的创建、切换、删除、保存和加载等功能,是应用程序中对话管理的核心组件。
它负责维护用户的对话历史并提供与UI交互的接口来管理对话列表。
"""
# 标准库导入
import tkinter as tk
import customtkinter as ctk
from datetime import datetime
import json
import os
import uuid
class ConversationManager:
"""对话管理器类
负责管理用户的对话历史,包括创建新对话、切换对话、删除对话、
保存对话历史到本地文件以及从本地文件加载对话历史等功能。
属性:
app: 应用程序的主实例
root: 应用程序的根窗口
conversation_listbox: 对话列表框组件(预留)
messages: 当前对话的消息列表
chat_histories: 所有对话历史的字典
current_conversation_id: 当前对话的ID
conversations: 所有对话ID的列表
update_chat_display: 更新聊天显示的方法引用
scroll_to_bottom: 滚动聊天窗口到底部的方法引用
default_chat_history_file: 默认聊天历史文件路径
"""
def __init__(self, app):
"""初始化对话管理器
参数:
app: 应用程序的主实例提供全局状态和UI访问
"""
# 应用程序引用
self.app = app
self.root = app.root
# 预留的UI组件引用
self.conversation_listbox = None
# 应用程序状态引用
self.messages = app.messages # 当前对话的消息列表
self.chat_histories = app.chat_histories # 所有对话历史的字典
self.current_conversation_id = app.current_conversation_id # 当前对话ID
self.conversations = app.conversations # 所有对话ID的列表
# 应用程序方法引用
self.update_chat_display = app.update_chat_display # 更新聊天显示的方法
self.scroll_to_bottom = app.scroll_to_bottom # 滚动到底部的方法
# 默认聊天历史文件路径
self.default_chat_history_file = "data/chat_history.json" # 默认保存位置
def create_new_conversation(self):
"""创建新对话
该方法执行以下操作:
1. 将当前消息列表同步到当前对话历史
2. 生成唯一的对话ID使用UUID
3. 清除当前消息列表
4. 添加默认欢迎消息
5. 创建新的对话记录并添加到聊天历史中
6. 更新对话列表显示
7. 更新聊天界面显示
8. 滚动到聊天窗口底部
9. 保存聊天历史到本地文件
"""
# 将当前消息列表同步到当前对话历史
if self.app.current_conversation_id and self.app.current_conversation_id in self.app.chat_histories:
self.app.chat_histories[self.app.current_conversation_id]["messages"] = self.app.messages.copy()
# 生成唯一的对话ID
conv_id = str(uuid.uuid4())
self.app.current_conversation_id = conv_id # 更新当前对话ID
# 添加默认欢迎消息
default_msg = {
"role": "assistant", # 消息角色:助手
"content": "你好我是DeepSeek AI助手有什么可以帮助你的吗", # 欢迎消息内容
"timestamp": datetime.now().isoformat() # 消息时间戳
}
# 清空当前消息列表并添加默认消息
self.app.messages.clear()
self.app.messages.append(default_msg)
# 创建新的对话记录
self.app.chat_histories[conv_id] = {
"title": "新对话", # 默认对话标题
"last_updated": datetime.now().isoformat(), # 最后更新时间
"messages": [default_msg.copy()] # 复制默认消息到对话历史
}
# 将新对话ID添加到对话列表
self.app.conversations.append(conv_id)
# 更新UI界面异步调用确保在主线程执行
self.root.after(0, self._update_conversation_list) # 更新对话列表显示
self.root.after(0, self.app.update_chat_display) # 更新聊天显示
self.root.after(0, self.app.scroll_to_bottom) # 滚动到底部
# 保存聊天历史(异步调用)
self.root.after(0, self._save_chat_history)
def switch_conversation(self, conv_id):
"""切换到指定对话
参数:
conv_id: 目标对话的唯一ID
该方法执行以下操作:
1. 将当前消息列表同步到当前对话历史
2. 更新当前对话ID
3. 清除当前消息列表
4. 将目标对话的消息复制到当前消息列表
5. 更新聊天界面显示
6. 保存聊天历史
"""
# 将当前消息列表同步到当前对话历史
if self.app.current_conversation_id and self.app.current_conversation_id in self.app.chat_histories:
self.app.chat_histories[self.app.current_conversation_id]["messages"] = self.app.messages.copy()
# 更新当前对话ID
self.app.current_conversation_id = conv_id
# 清空当前消息列表
self.app.messages.clear()
# 将目标对话的消息复制到当前消息列表
self.app.messages.extend(self.app.chat_histories[conv_id]["messages"])
# 更新聊天界面显示(异步调用)
self.root.after(0, self.app.update_chat_display)
# 更新对话列表显示(确保选中当前对话)(异步调用)
self.root.after(0, self._update_conversation_list)
# 保存聊天历史(异步调用)
self.root.after(0, self._save_chat_history)
def _update_conversation_list(self):
"""更新对话列表显示
该方法执行以下操作:
1. 检查UI组件是否已经初始化
2. 清空对话列表框
3. 按最后更新时间降序排序对话
4. 在列表框中显示所有对话显示标题和ID的前8位
5. 选中并激活当前对话
"""
# 安全检查确保UI组件已经初始化
if not hasattr(self.app, 'ui_components'):
return
# 使用get_conversation_listbox()方法获取对话列表框
conversation_listbox = self.app.ui_components.get_conversation_listbox()
if not conversation_listbox:
return
# 清空对话列表框
conversation_listbox.delete(0, tk.END)
try:
# 确保数据一致性过滤掉self.app.conversations中不存在于self.app.chat_histories的对话ID
valid_conversations = [conv_id for conv_id in self.app.conversations
if conv_id in self.app.chat_histories]
# 如果发现不一致的数据更新self.app.conversations
if len(valid_conversations) != len(self.app.conversations):
self.app.conversations = valid_conversations
# 检查logger_manager是否存在
if hasattr(self.app, 'logger_manager'):
self.app.logger_manager.log("warning", "修复了对话列表与聊天历史之间的数据不一致问题")
# 确保时间戳统一格式转换为datetime对象以避免类型错误
def ensure_datetime(timestamp):
if isinstance(timestamp, datetime):
return timestamp
try:
return datetime.fromisoformat(timestamp)
except:
return datetime.now()
# 按最后更新时间降序排序对话
sorted_conversations = sorted(
valid_conversations,
key=lambda conv_id: ensure_datetime(self.app.chat_histories[conv_id]["last_updated"]),
reverse=True
)
# 更新对话列表显示
for conv_id in sorted_conversations:
title = self.app.chat_histories[conv_id]["title"]
# 显示标题和唯一ID的前8位便于用户识别
display_text = f"{title} [ID: {conv_id[:8]}]"
conversation_listbox.insert(tk.END, display_text)
# 选中并激活当前对话
if self.app.current_conversation_id:
try:
# 获取当前对话在排序后列表中的索引
index = sorted_conversations.index(self.app.current_conversation_id)
# 选中当前对话
conversation_listbox.select_set(index)
# 激活当前对话(滚动到可见位置)
conversation_listbox.activate(index)
except ValueError:
# 如果当前对话ID不在列表中忽略错误
pass
except Exception as e:
# 记录错误日志
if hasattr(self.app, 'logger_manager'):
self.app.logger_manager.log("error", f"更新对话列表失败: {e}")
# 即使出错,也尝试显示可用的对话
try:
for conv_id in self.app.conversations:
if conv_id in self.app.chat_histories:
conversation_listbox.insert(tk.END, f"对话 {conv_id[:8]}")
except:
# 如果再次出错,至少显示一个默认消息
conversation_listbox.insert(tk.END, "对话列表加载失败")
def delete_conversation(self):
"""删除当前选中的对话
该方法执行以下操作:
1. 检查UI组件是否已经初始化
2. 获取当前选中的对话
3. 检查是否还有其他对话(不能删除最后一个对话)
4. 删除对话记录和对话ID
5. 如果删除的是当前对话,切换到第一个剩余对话
6. 更新对话列表显示
7. 更新聊天界面显示
8. 保存更新后的聊天历史
"""
# 安全检查确保UI组件已经初始化
if not hasattr(self.app, 'ui_components') or not hasattr(self.app.ui_components, 'conversation_listbox'):
return
# 获取当前选中的对话索引
selection = self.app.ui_components.conversation_listbox.curselection()
if not selection: # 如果没有选中任何对话,直接返回
return
try:
# 获取选中的对话ID需要考虑排序后的列表
index = selection[0]
# 确保数据一致性过滤掉self.app.conversations中不存在于self.app.chat_histories的对话ID
valid_conversations = [conv_id for conv_id in self.app.conversations
if conv_id in self.app.chat_histories]
# 确保时间戳统一格式转换为datetime对象以避免类型错误
def ensure_datetime(timestamp):
if isinstance(timestamp, datetime):
return timestamp
try:
return datetime.fromisoformat(timestamp)
except:
return datetime.now()
# 按最后更新时间降序排序对话与_update_conversation_list保持一致
sorted_conversations = sorted(
valid_conversations,
key=lambda conv_id: ensure_datetime(self.app.chat_histories[conv_id]["last_updated"]),
reverse=True
)
# 从排序后的列表中获取对话ID
conv_id = sorted_conversations[index]
# 保护机制:不能删除最后一个对话
if len(self.app.conversations) == 1:
return
# 删除对话记录
del self.app.chat_histories[conv_id] # 从聊天历史中删除
self.app.conversations.remove(conv_id) # 从对话列表中删除
# 如果删除的是当前对话,切换到第一个剩余对话
if conv_id == self.app.current_conversation_id:
self.switch_conversation(self.app.conversations[0])
# 更新UI界面
self._update_conversation_list() # 更新对话列表显示
self.app.update_chat_display() # 更新聊天显示
# 保存更新后的聊天历史
self._save_chat_history()
except (ValueError, KeyError):
# 如果出现错误如对话ID不存在忽略错误并返回
return
def clear_chat(self):
"""清空当前聊天记录
该方法执行以下操作:
1. 清除当前消息列表
2. 清除当前对话的所有消息记录
3. 更新当前对话的最后更新时间
4. 更新聊天界面显示
5. 保存更新后的聊天历史
6. 更新对话列表显示
"""
# 清除当前消息列表
self.app.messages.clear()
# 如果存在当前对话,清除其消息记录并更新最后更新时间
if self.app.current_conversation_id:
self.app.chat_histories[self.app.current_conversation_id]["messages"] = []
self.app.chat_histories[self.app.current_conversation_id]["last_updated"] = datetime.now().isoformat()
# 更新UI界面
self.app.update_chat_display() # 更新聊天显示
# 保存更新后的聊天历史
self._save_chat_history()
# 更新对话列表(反映最后更新时间的变化)
self._update_conversation_list()
def _save_chat_history(self):
"""保存聊天历史到本地文件
该方法执行以下操作:
1. 确保data目录存在
2. 根据当前登录用户选择合适的文件名(每个用户有独立的历史文件)
3. 准备要保存的数据结构
4. 遍历所有对话,筛选并处理要保存的消息
5. 将数据写入JSON文件
注意:
- 不保存"思考中"状态的消息
- 使用UTF-8编码确保中文正常显示
- 格式化JSON输出以便于人类阅读
"""
try:
# 确保data目录存在如果不存在则创建
os.makedirs("data", exist_ok=True)
# 获取当前登录用户名
current_user = self.app.current_username
if not current_user:
# 如果没有登录用户,使用默认文件名
chat_history_file = self.default_chat_history_file
else:
# 为每个用户创建一个单独的聊天历史文件使用os.path.join确保跨平台兼容
chat_history_file = os.path.join("data", f"chat_history_{current_user}.json")
# 准备要保存的数据结构
chat_data = {
"last_updated": datetime.now().isoformat(), # 保存时间
"current_conversation_id": self.app.current_conversation_id, # 当前对话ID
"conversations": self.app.conversations.copy(), # 对话列表顺序
"chat_histories": [] # 对话历史列表
}
# 遍历所有对话并保存
for conv_id, conv in self.app.chat_histories.items():
# 确保时间戳是字符串格式
last_updated = conv["last_updated"]
if hasattr(last_updated, "isoformat"):
last_updated = last_updated.isoformat()
conv_data = {
"id": conv_id, # 对话ID
"title": conv["title"], # 对话标题
"last_updated": last_updated, # 最后更新时间(确保是字符串)
"messages": [] # 消息列表
}
# 只保存用户和助手的实际消息,不保存"思考中"状态的临时消息
for msg in conv["messages"]:
if msg.get("thinking"): # 跳过思考中状态的消息
continue
# 确保时间戳是字符串格式
timestamp = msg["timestamp"]
if hasattr(timestamp, "isoformat"):
timestamp = timestamp.isoformat()
# 复制消息的关键字段
conv_data["messages"].append({
"role": msg["role"], # 角色user/assistant
"content": msg["content"], # 消息内容
"timestamp": timestamp # 时间戳(确保是字符串)
})
# 将对话添加到保存数据中
chat_data["chat_histories"].append(conv_data)
# 确保保存的是对话ID列表而不是对话数据
chat_data["conversations"] = self.app.conversations.copy()
# 将数据写入JSON文件
with open(chat_history_file, "w", encoding="utf-8") as f:
json.dump(chat_data, f, ensure_ascii=False, indent=2)
except Exception as e:
# 打印错误信息但不中断程序
print(f"保存聊天历史失败: {e}")
def load_chat_history(self):
"""从本地文件加载聊天历史
该方法执行以下操作:
1. 确保data目录存在
2. 根据当前登录用户选择合适的历史文件
3. 检查历史文件是否存在
4. 如果存在加载并解析JSON数据
5. 清空现有对话和消息
6. 加载所有对话和消息
7. 处理时间戳格式转换
8. 更新对话列表显示
9. 兼容旧版本数据结构
10. 如果没有历史记录或加载失败,创建新对话
异常处理:
- 处理文件不存在的情况
- 处理JSON解析错误
- 处理时间戳格式错误
- 兼容旧版本数据结构
"""
try:
# 确保data目录存在如果不存在则创建
os.makedirs("data", exist_ok=True)
# 获取当前登录用户名
current_user = self.app.current_username
if not current_user:
# 如果没有登录用户,使用默认文件名
chat_history_file = self.default_chat_history_file
else:
# 为每个用户创建一个单独的聊天历史文件
chat_history_file = f"data/chat_history_{current_user}.json"
# 检查历史文件是否存在,并且不是目录
if os.path.exists(chat_history_file) and not os.path.isdir(chat_history_file):
try:
# 读取并解析JSON文件
with open(chat_history_file, "r", encoding="utf-8") as f:
chat_data = json.load(f)
except json.JSONDecodeError as e:
# JSON解析错误创建新对话
print(f"JSON解析错误: {e}")
self.create_new_conversation()
return
except Exception as e:
# 其他文件读取错误,创建新对话
print(f"读取聊天历史文件失败: {e}")
self.create_new_conversation()
return
else:
# 文件不存在或为目录,创建新对话
self.create_new_conversation()
return
# 清空现有对话和消息
self.app.conversations.clear()
self.app.chat_histories.clear()
# 加载所有对话历史
for conv in chat_data.get("chat_histories", []):
conv_id = conv.get("id")
if conv_id:
# 处理对话中的消息
messages = []
for msg in conv.get("messages", []):
# 确保消息角色有效
if msg.get("role") not in ["user", "assistant"]:
continue
# 确保消息内容不为空
if not msg.get("content"):
continue
# 确保时间戳存在
if not msg.get("timestamp"):
msg["timestamp"] = datetime.now().isoformat()
try:
# 将时间戳字符串转换为datetime对象
msg["timestamp"] = datetime.fromisoformat(msg["timestamp"])
except:
# 如果解析失败,使用当前时间
msg["timestamp"] = datetime.now()
messages.append(msg) # 添加到消息列表
# 处理对话的最后更新时间
try:
last_updated = datetime.fromisoformat(conv["last_updated"])
except:
last_updated = datetime.now()
# 将对话添加到聊天历史
self.app.chat_histories[conv_id] = {
"title": conv["title"], # 对话标题
"last_updated": last_updated, # 最后更新时间
"messages": messages # 消息列表
}
# 加载对话列表顺序
saved_conversations = chat_data.get("conversations", [])
if saved_conversations:
# 只保留存在于chat_histories中的对话ID
for conv_id in saved_conversations:
if conv_id in self.app.chat_histories:
self.app.conversations.append(conv_id)
# 更新对话列表显示
self._update_conversation_list()
# 设置当前对话ID和加载对应的消息列表
if self.app.conversations:
# 尝试加载保存的当前对话ID
saved_current_id = chat_data.get("current_conversation_id")
if saved_current_id and saved_current_id in self.app.conversations:
self.app.current_conversation_id = saved_current_id
else:
# 如果保存的当前对话ID无效使用第一个对话
self.app.current_conversation_id = self.app.conversations[0]
# 加载当前对话的消息列表
self.app.messages.clear()
self.app.messages.extend(self.app.chat_histories[self.app.current_conversation_id]["messages"])
# 更新聊天显示
self.app.update_chat_display()
self.app.scroll_to_bottom()
else:
# 兼容旧版本数据结构(只有一个对话)或没有对话时
self.create_new_conversation() # 创建新对话
# 加载旧版本的消息
for msg in chat_data.get("messages", []):
try:
# 将时间戳字符串转换为datetime对象
msg["timestamp"] = datetime.fromisoformat(msg["timestamp"])
except:
# 如果解析失败,使用当前时间
msg["timestamp"] = datetime.now()
# 添加到当前消息列表和聊天历史
self.app.messages.append(msg)
self.app.chat_histories[self.app.current_conversation_id]["messages"].append(msg)
# 更新对话列表
self._update_conversation_list()
# 保存聊天历史(确保数据一致性)
self._save_chat_history()
except Exception as e:
# 处理任何未捕获的异常
print(f"加载聊天历史失败: {e}")
# 如果加载失败,创建新对话
self.create_new_conversation()