using System.Text.RegularExpressions; using Nexus.Api.Models; namespace Nexus.Api.Services; public sealed partial class IncidentService( IOpenClawAgentConfigurationService configuration) : IIncidentService { private const string IncidentDirectory = "memory/incidents"; public async Task> GetAllAsync( string agentId, CancellationToken cancellationToken = default) { var normalizedAgentId = NormalizeAgentId(agentId); OpenClawWorkspaceCollectionDto listing; try { listing = await configuration.GetWorkspaceAsync( normalizedAgentId, IncidentDirectory, 0, OpenClawContentReadHelpers.MaxFiles, cancellationToken); } catch (OpenClawGatewayRpcException exception) when (OpenClawContentReadHelpers.IsNotFound(exception)) { return []; } var entries = listing.Entries .Where(OpenClawContentReadHelpers.IsMarkdownFile) .OrderByDescending(entry => entry.Name, StringComparer.OrdinalIgnoreCase); return await OpenClawContentReadHelpers.SelectBoundedAsync< OpenClawWorkspaceEntryDto, IncidentSummary>( entries, async (entry, token) => { var file = await configuration.GetWorkspaceFileAsync( normalizedAgentId, entry.Path, token); var content = OpenClawContentReadHelpers.ReadText(file); if (content is null) return null; var baseName = Path.GetFileNameWithoutExtension(file.Name); return new IncidentSummary( file.Name, ExtractTitle(baseName, content), ExtractDate(file.Name), ExtractSeverity(content), ExtractExcerpt(content), file.Size, normalizedAgentId, file.Path); }, cancellationToken); } public async Task GetByNameAsync( string name, string agentId, CancellationToken cancellationToken = default) { if (!OpenClawContentReadHelpers.IsSafeFileName(name)) return null; var normalizedAgentId = NormalizeAgentId(agentId); var fileName = name.EndsWith(".md", StringComparison.OrdinalIgnoreCase) ? name : name + ".md"; try { var file = await configuration.GetWorkspaceFileAsync( normalizedAgentId, $"{IncidentDirectory}/{fileName}", cancellationToken); var content = OpenClawContentReadHelpers.ReadText(file); if (content is null) return null; return new IncidentDetail( file.Name, ExtractTitle( Path.GetFileNameWithoutExtension(file.Name), content), ExtractDate(file.Name), content, file.Size, normalizedAgentId, file.Path); } catch (OpenClawGatewayRpcException exception) when (OpenClawContentReadHelpers.IsNotFound(exception)) { return null; } } private static string NormalizeAgentId(string? agentId) => string.IsNullOrWhiteSpace(agentId) ? "iris" : agentId.Trim().ToLowerInvariant(); private static string ExtractTitle(string name, string content) { var match = TitleRegex().Match(content); return match.Success ? match.Groups[1].Value.Trim() : name; } private static string? ExtractDate(string fileName) { var match = DateRegex().Match(fileName); return match.Success ? match.Groups[1].Value : null; } private static string ExtractSeverity(string content) { var match = SeverityRegex().Match(content); return match.Success ? match.Groups[1].Value.Trim() : "unknown"; } private static string ExtractExcerpt(string content) { var excerptEnd = content.IndexOf("\n## ", StringComparison.Ordinal); var excerpt = excerptEnd > 0 ? content[..excerptEnd].Trim() : content[..Math.Min(300, content.Length)].Trim(); return excerpt.Length > 200 ? excerpt[..200] + "…" : excerpt; } [GeneratedRegex(@"^#\s+(.+)$", RegexOptions.Multiline)] private static partial Regex TitleRegex(); [GeneratedRegex(@"^(\d{4}-\d{2}-\d{2})")] private static partial Regex DateRegex(); [GeneratedRegex(@"\*\*Severity:\*\*\s*(.+)$", RegexOptions.Multiline)] private static partial Regex SeverityRegex(); }