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#

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.

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);
}
}
}