# 插件系统 > 所属项目: free-code .NET 10 重写 > 所属模块: [UI 与扩展设计](UI与扩展设计.md) > 原始代码: `../../src/plugins/` --- ## 概述 插件系统允许第三方代码在运行时扩展 free-code 的能力,包括注册新技能、新斜杠命令和新 MCP 服务器配置。原始实现通过 Node.js 的动态 `import()` 加载插件模块,利用模块缓存隔离各插件的命名空间。 .NET 重写用 `AssemblyLoadContext`(ALC)替代动态导入,实现更强的隔离性和确定性卸载。每个插件运行在独立的 `PluginLoadContext` 实例中,其依赖项从插件目录优先解析,不与宿主进程或其他插件共享程序集版本。`PluginManager` 管理所有插件的生命周期,支持安装、卸载、启用和禁用操作。 --- ## 17.1 接口定义 ```csharp /// /// 插件管理器 — AssemblyLoadContext 隔离加载 /// 对应原始 plugin system /// public interface IPluginManager { Task> LoadAllPluginsAsync(); Task InstallPluginAsync(string pluginId); Task UninstallPluginAsync(string pluginId); Task EnablePluginAsync(string pluginId); Task DisablePluginAsync(string pluginId); Task RefreshAsync(); } ``` --- ## 17.2 LoadedPlugin 与 PluginManifest ```csharp /// /// 已加载的插件实例描述 /// public sealed record LoadedPlugin { /// 插件唯一标识符(目录名) public required string Id { get; init; } /// 插件显示名称 public required string Name { get; init; } /// 版本字符串 public required string Version { get; init; } /// 来源:marketplace | local | github public required string Source { get; init; } /// 主程序集路径 public required string AssemblyPath { get; init; } /// 是否启用 public bool IsEnabled { get; init; } /// 插件清单 public PluginManifest Manifest { get; init; } = new(); } /// /// 插件清单 — plugin.json 反序列化目标 /// 描述插件向宿主暴露的所有扩展点 /// public sealed record PluginManifest { /// 插件提供的技能列表 public IReadOnlyList Skills { get; init; } = []; /// 插件提供的斜杠命令列表 public IReadOnlyList Commands { get; init; } = []; /// 插件注册的 MCP 服务器配置(服务器名 → 配置) public IReadOnlyDictionary McpServers { get; init; } = new Dictionary(); } ``` **设计意图** `PluginManifest` 直接对应 `plugin.json` 的顶层结构,三个扩展点(技能、命令、MCP 服务器)分别映射为 `SkillLoader`、`CommandRegistry`、`McpClientManager` 的动态注册入口。插件卸载时,这三个注册表各自移除该插件贡献的条目。 --- ## 17.3 PluginLoadContext ```csharp /// /// 可卸载的程序集加载上下文 /// 对应原始动态 import() 的隔离语义 /// public sealed class PluginLoadContext : AssemblyLoadContext { private readonly AssemblyDependencyResolver _resolver; public PluginLoadContext(string pluginPath) : base(isCollectible: true) { _resolver = new AssemblyDependencyResolver(pluginPath); } protected override Assembly? Load(AssemblyName assemblyName) { // 优先从插件目录解析,回退到默认加载上下文 var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); return assemblyPath != null ? LoadFromAssemblyPath(assemblyPath) : null; // null 触发回退到默认 ALC } protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); return libraryPath != null ? LoadUnmanagedDllFromPath(libraryPath) : IntPtr.Zero; } } ``` **设计意图** `isCollectible: true` 是关键参数。普通 `AssemblyLoadContext` 加载的程序集在整个进程生命周期内无法卸载。可回收 ALC 允许在所有对该上下文中类型的强引用归零后,GC 完整回收其托管内存和元数据,这是 `/plugin uninstall` 命令实现热卸载的基础。 `AssemblyDependencyResolver` 读取插件主程序集旁边的 `.deps.json` 文件,自动解析所有传递依赖的路径。插件作者只需将所有依赖打包到插件目录,无需关心宿主进程中已有哪些程序集版本。 --- ## 17.4 PluginManager 实现 ```csharp public sealed class PluginManager : IPluginManager { private readonly ConcurrentDictionary _contexts = new(); private readonly ConcurrentDictionary _plugins = new(); public async Task> LoadAllPluginsAsync() { var pluginDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".free-code", "plugins"); if (!Directory.Exists(pluginDir)) return []; foreach (var dir in Directory.GetDirectories(pluginDir)) { var manifestPath = Path.Combine(dir, "plugin.json"); if (!File.Exists(manifestPath)) continue; var manifest = JsonSerializer.Deserialize( await File.ReadAllTextAsync(manifestPath)); var dllPath = Directory.GetFiles(dir, "*.dll").FirstOrDefault(); if (dllPath == null) continue; var context = new PluginLoadContext(dllPath); var assembly = context.LoadFromAssemblyPath(dllPath); _contexts[dir] = context; _plugins[dir] = new LoadedPlugin { Id = Path.GetFileName(dir), Name = manifest?.Skills.FirstOrDefault()?.Name ?? Path.GetFileName(dir), Version = "1.0", Source = "local", AssemblyPath = dllPath, IsEnabled = true, }; } return _plugins.Values.ToList(); } public async Task UninstallPluginAsync(string pluginId) { if (_contexts.TryRemove(pluginId, out var context)) { context.Unload(); // 触发 GC 可回收流程 } _plugins.TryRemove(pluginId, out _); } public Task RefreshAsync() { // 重新扫描目录,加载新插件,卸载已删除的 return LoadAllPluginsAsync(); } public Task InstallPluginAsync(string pluginId) { // 从 marketplace / GitHub 下载到 ~/.free-code/plugins// // 然后调用 LoadAllPluginsAsync 触发加载 throw new NotImplementedException("待实现:marketplace 下载逻辑"); } public Task EnablePluginAsync(string pluginId) { if (_plugins.TryGetValue(pluginId, out var plugin)) _plugins[pluginId] = plugin with { IsEnabled = true }; return Task.CompletedTask; } public Task DisablePluginAsync(string pluginId) { if (_plugins.TryGetValue(pluginId, out var plugin)) _plugins[pluginId] = plugin with { IsEnabled = false }; return Task.CompletedTask; } } ``` --- ## 插件目录结构 ``` ~/.free-code/plugins/ └── my-plugin/ ├── plugin.json # 插件清单 ├── MyPlugin.dll # 主程序集 ├── MyPlugin.deps.json # 依赖清单(供 AssemblyDependencyResolver 使用) └── SomeDependency.dll # 插件私有依赖 ``` ### plugin.json 示例 ```json { "skills": [ { "name": "my-skill", "description": "自定义技能示例", "content": "你是一个帮助用户完成特定任务的助手。" } ], "commands": [], "mcpServers": { "my-mcp": { "command": "npx", "args": ["-y", "my-mcp-server"], "scope": "local" } } } ``` --- ## 生命周期图 ``` LoadAllPluginsAsync() │ ├── 扫描 ~/.free-code/plugins/ 目录 ├── 读取 plugin.json → 反序列化 PluginManifest ├── 创建 PluginLoadContext(dllPath) ├── LoadFromAssemblyPath(dllPath) └── 注册到 _plugins / _contexts UninstallPluginAsync(id) │ ├── context.Unload() [ALC 标记为待回收] ├── 移除 _contexts[id] [WeakReference 归零后 GC 回收] └── 移除 _plugins[id] [插件对象消失] ``` --- ## 参考资料 - [UI 与扩展设计 — 总览](UI与扩展设计.md) - [UI 与扩展设计 — 技能系统](UI与扩展设计-技能系统.md) - [核心模块设计 — 命令系统](../核心模块设计/核心模块设计-命令系统.md) - [基础设施设计 — MCP 协议集成](../基础设施设计/基础设施设计-MCP协议集成.md)