using Nexus.Api.Helpers; namespace Nexus.Api.Services; public sealed class DocService : IDocService { private static readonly string[] AllowedExtensions = [".md", ".json", ".txt", ".yaml", ".yml", ".html", ".css"]; private static readonly string[] SearchRoots = [ "/mnt/workspace-iris", "/home/node/.openclaw/workspace/nexus" ]; private static readonly (string Dir, string Category)[] ScanDirectories = [ ("/mnt/workspace-iris/nexus-phases", "phases"), ("/mnt/workspace-iris/skills", "skills"), ("/mnt/workspace-iris", "workspace"), ("/home/node/.openclaw/workspace/nexus", "nexus"), ("/home/node/.openclaw/workspace/nexus/phases", "nexus-phases") ]; public IReadOnlyList GetAll() { var results = new List(); foreach (var (dir, category) in ScanDirectories) { if (!Directory.Exists(dir)) continue; foreach (var file in Directory.GetFiles(dir, "*.*")) { var ext = Path.GetExtension(file).ToLowerInvariant(); if (!AllowedExtensions.Contains(ext)) continue; var fi = new FileInfo(file); results.Add(new DocFileInfo( fi.Name, file.Replace("/mnt/workspace-iris", "").TrimStart('/'), category, ext.Replace(".", ""), fi.Length, fi.LastWriteTimeUtc)); } } return results.OrderByDescending(x => x.ModifiedAt).Take(100).ToList(); } public async Task GetFileAsync(string path) { if (string.IsNullOrWhiteSpace(path)) return null; string? resolvedPath = null; foreach (var root in SearchRoots) { if (PathSecurityHelper.TryResolveSafePath(root, path, out var candidate) && File.Exists(candidate)) { resolvedPath = candidate; break; } } if (resolvedPath is null) return null; var content = await File.ReadAllTextAsync(resolvedPath); var fi = new FileInfo(resolvedPath); var relativePath = resolvedPath .Replace("/mnt/workspace-iris/", "") .Replace("/home/node/.openclaw/workspace/nexus/", ""); return new DocFileContent(fi.Name, relativePath, content, fi.Length, fi.LastWriteTimeUtc); } }