# 插件系统
> 所属项目: 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)