|
|
|
|
|
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>
|
|
|
|
|
|
/// 会话文件持久化 DTO(YAML 格式:元数据 + 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|