using System.Text.Json; using MAF1.PluginContract; namespace MAF1.Plugins; /// /// 扫描 exe 旁边的 plugins/*/plugin.json。源码目录 Plugins/ 不会被运行时直接读到,要先 build 复制到输出目录。 /// public sealed class PluginScanner(PluginOptions options) { public PluginScanResult Scan() { string root = options.ResolveRoot(); Directory.CreateDirectory(root); List plugins = []; List issues = []; HashSet ids = new(StringComparer.OrdinalIgnoreCase); foreach (string folder in Directory.GetDirectories(root)) { string folderName = Path.GetFileName(folder); if (folderName.StartsWith('_') || folderName.StartsWith('.')) { continue; } string manifestPath = Path.Combine(folder, "plugin.json"); if (!File.Exists(manifestPath)) { issues.Add(new CatalogIssue { Level = "warning", Source = folderName, Message = "目录里没有 plugin.json,已跳过。", }); continue; } PluginManifest? manifest; try { string json = File.ReadAllText(manifestPath); manifest = JsonSerializer.Deserialize(json, PluginJson.Options); } catch (Exception ex) { issues.Add(new CatalogIssue { Level = "error", Source = folderName, Message = $"plugin.json 无法解析:{ex.Message}", }); continue; } if (manifest is null || string.IsNullOrWhiteSpace(manifest.Id)) { issues.Add(new CatalogIssue { Level = "error", Source = folderName, Message = "plugin.json 缺少 id。", }); continue; } if (string.IsNullOrWhiteSpace(manifest.Launch.Command)) { issues.Add(new CatalogIssue { Level = "error", Source = manifest.Id, Message = "plugin.json 缺少 launch.command,宿主不会替插件拼命令。", }); continue; } if (!ids.Add(manifest.Id)) { issues.Add(new CatalogIssue { Level = "error", Source = manifest.Id, Message = $"插件 id `{manifest.Id}` 重复,已忽略目录 {folderName}。", }); continue; } if (DuplicatePortNames(manifest.Inputs)) { issues.Add(new CatalogIssue { Level = "error", Source = manifest.Id, Message = "inputs 字段名重复。" }); continue; } if (DuplicatePortNames(manifest.Outputs)) { issues.Add(new CatalogIssue { Level = "error", Source = manifest.Id, Message = "outputs 字段名重复。" }); continue; } plugins.Add(new LoadedPlugin { FolderName = folderName, FolderPath = folder, Manifest = manifest, }); } return new PluginScanResult(root, plugins, issues); } private static bool DuplicatePortNames(List ports) => ports.GroupBy(p => p.Name, StringComparer.OrdinalIgnoreCase).Any(g => g.Count() > 1); } public sealed record PluginScanResult( string Root, IReadOnlyList Plugins, IReadOnlyList Issues);