You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kev/Drawer/AI/Service/SessionStorage.cs

181 lines
5.7 KiB
C#

1 month ago
using System;
using System.Collections.Generic;
using System.IO;
using AI.Models;
using AI.Models.Store;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace AI.Service
{
/// <summary>
/// 会话文件持久化 DTOYAML 格式:元数据 + entries
/// </summary>
public class SessionFileDto
{
public string SessionId { get; set; } = string.Empty;
public string Title { get; set; } = "新会话";
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public string WorkflowMode { get; set; } = "Ask";
public List<ConversationEntryDto> Entries { get; set; } = new();
}
/// <summary>
/// 会话存储的 YAML 文件持久化:按会话 ID 保存到目录下的 {sessionId}.yaml支持加载全部会话。
/// </summary>
public class SessionStorage
{
private readonly string _directory;
private static readonly ISerializer _serializer = new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
private static readonly IDeserializer _deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();
public SessionStorage(string? baseDirectory = null)
{
_directory = string.IsNullOrWhiteSpace(baseDirectory)
? Path.Combine(AppContext.BaseDirectory, "Sessions")
: Path.Combine(baseDirectory, "Sessions");
}
/// <summary>
/// 确保会话目录存在
/// </summary>
public void EnsureDirectory()
{
if (!Directory.Exists(_directory))
Directory.CreateDirectory(_directory);
}
/// <summary>
/// 获取指定会话的 YAML 文件路径
/// </summary>
public string GetFilePath(string sessionId)
{
EnsureDirectory();
return Path.Combine(_directory, $"{sessionId}.yaml");
}
/// <summary>
/// 将会话保存为 YAML 文件
/// </summary>
public void Save(ChatSession session)
{
if (session == null) return;
var dto = new SessionFileDto
{
SessionId = session.Id,
Title = session.Title ?? "新会话",
CreatedAt = session.CreatedAt,
UpdatedAt = session.UpdatedAt,
WorkflowMode = session.WorkflowMode.ToString(),
Entries = ConversationEntryMapper.ToDtoList(session.Store.Entries)
};
var yaml = _serializer.Serialize(dto);
var path = GetFilePath(session.Id);
File.WriteAllText(path, yaml);
}
/// <summary>
/// 从 YAML 文件加载一个会话;文件不存在或解析失败时返回 null
/// </summary>
public ChatSession? Load(string sessionId)
{
var path = GetFilePath(sessionId);
if (!File.Exists(path))
{
return null;
}
try
{
var yaml = File.ReadAllText(path);
return LoadFromYaml(yaml);
}
catch
{
return null;
}
}
/// <summary>
/// 从 YAML 字符串反序列化并构造 ChatSession用于单文件加载或测试
/// </summary>
public ChatSession? LoadFromYaml(string yaml)
{
if (string.IsNullOrWhiteSpace(yaml))
{
return null;
}
try
{
var dto = _deserializer.Deserialize<SessionFileDto>(yaml);
if (dto == null)
{
return null;
}
var store = new ConversationStore();
store.LoadFromEntries(dto.Entries ?? new List<ConversationEntryDto>());
var mode = WorkflowMode.Ask;
if (!string.IsNullOrEmpty(dto.WorkflowMode) && Enum.TryParse<WorkflowMode>(dto.WorkflowMode, true, out var m))
{
mode = m;
}
return new ChatSession(
dto.SessionId,
dto.Title,
dto.CreatedAt,
dto.UpdatedAt,
mode,
store);
}
catch
{
return null;
}
}
/// <summary>
/// 从会话目录加载所有会话文件,返回按 UpdatedAt 倒序的会话列表
/// </summary>
public IEnumerable<ChatSession> LoadAll()
{
EnsureDirectory();
var list = new List<ChatSession>();
foreach (var path in Directory.EnumerateFiles(_directory, "*.yaml"))
{
try
{
var yaml = File.ReadAllText(path);
var session = LoadFromYaml(yaml);
if (session != null)
{
list.Add(session);
}
}
catch
{
// 单文件失败则跳过
}
}
list.Sort((a, b) => b.UpdatedAt.CompareTo(a.UpdatedAt));
return list;
}
/// <summary>
/// 删除指定会话的持久化文件
/// </summary>
public void Delete(string sessionId)
{
var path = GetFilePath(sessionId);
if (File.Exists(path))
File.Delete(path);
}
}
}