using Nexus.Api.Models; namespace Nexus.Api.Services; public sealed class DocService( IOpenClawAgentConfigurationService configuration) : IDocService { private static readonly HashSet AllowedExtensions = new( [".md", ".json", ".txt", ".yaml", ".yml", ".html", ".css"], StringComparer.OrdinalIgnoreCase); private static readonly (string Path, string Category)[] ScanDirectories = [ ("", "workspace"), ("nexus-phases", "phases"), ("skills", "skills"), ("nexus", "nexus"), ("nexus/phases", "nexus-phases") ]; public async Task> GetAllAsync( string agentId, CancellationToken cancellationToken = default) { var normalizedAgentId = NormalizeAgentId(agentId); var directories = await OpenClawContentReadHelpers.SelectBoundedAsync< (string Path, string Category), DirectoryResult>( ScanDirectories, async (directory, token) => { try { var listing = await configuration.GetWorkspaceAsync( normalizedAgentId, directory.Path, 0, 100, token); return new DirectoryResult( directory.Category, listing); } catch (OpenClawGatewayRpcException exception) when (OpenClawContentReadHelpers.IsNotFound(exception)) { return null; } }, cancellationToken); return directories .SelectMany(directory => directory.Listing.Entries .Where(entry => string.Equals(entry.Kind, "file", StringComparison.Ordinal) && !(string.IsNullOrEmpty(directory.Listing.Path) && string.Equals( entry.Name, "MEMORY.md", StringComparison.OrdinalIgnoreCase)) && AllowedExtensions.Contains( Path.GetExtension(entry.Name))) .Select(entry => new DocFileInfo( entry.Name, entry.Path, directory.Category, Path.GetExtension(entry.Name).TrimStart('.') .ToLowerInvariant(), entry.Size ?? 0, (entry.UpdatedAt ?? directory.Listing.CheckedAt).UtcDateTime, normalizedAgentId, entry.Path))) .OrderByDescending(item => item.ModifiedAt) .Take(100) .ToArray(); } public async Task GetFileAsync( string path, string agentId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(path) || !AllowedExtensions.Contains(Path.GetExtension(path))) { return null; } var normalizedAgentId = NormalizeAgentId(agentId); try { var file = await configuration.GetWorkspaceFileAsync( normalizedAgentId, path, cancellationToken); var content = OpenClawContentReadHelpers.ReadText(file); if (content is null || !AllowedExtensions.Contains(Path.GetExtension(file.Name))) { return null; } return new DocFileContent( file.Name, file.Path, content, file.Size, (file.UpdatedAt ?? file.CheckedAt).UtcDateTime, 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 sealed record DirectoryResult( string Category, OpenClawWorkspaceCollectionDto Listing); }