free-code-dotnet/docs/UI与扩展设计/UI与扩展设计-插件系统.md
应文浩wenhao.ying@xiaobao100.com e25ac591a7 init easy-code
2026-04-06 07:24:24 +08:00

281 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 插件系统
> 所属项目: free-code .NET 10 重写
> 所属模块: [UI 与扩展设计](UI与扩展设计.md)
> 原始代码: `../../src/plugins/`
---
## 概述
插件系统允许第三方代码在运行时扩展 free-code 的能力,包括注册新技能、新斜杠命令和新 MCP 服务器配置。原始实现通过 Node.js 的动态 `import()` 加载插件模块,利用模块缓存隔离各插件的命名空间。
.NET 重写用 `AssemblyLoadContext`ALC替代动态导入实现更强的隔离性和确定性卸载。每个插件运行在独立的 `PluginLoadContext` 实例中,其依赖项从插件目录优先解析,不与宿主进程或其他插件共享程序集版本。`PluginManager` 管理所有插件的生命周期,支持安装、卸载、启用和禁用操作。
---
## 17.1 接口定义
```csharp
/// <summary>
/// 插件管理器 — AssemblyLoadContext 隔离加载
/// 对应原始 plugin system
/// </summary>
public interface IPluginManager
{
Task<IReadOnlyList<LoadedPlugin>> LoadAllPluginsAsync();
Task InstallPluginAsync(string pluginId);
Task UninstallPluginAsync(string pluginId);
Task EnablePluginAsync(string pluginId);
Task DisablePluginAsync(string pluginId);
Task RefreshAsync();
}
```
---
## 17.2 LoadedPlugin 与 PluginManifest
```csharp
/// <summary>
/// 已加载的插件实例描述
/// </summary>
public sealed record LoadedPlugin
{
/// <summary>插件唯一标识符(目录名)</summary>
public required string Id { get; init; }
/// <summary>插件显示名称</summary>
public required string Name { get; init; }
/// <summary>版本字符串</summary>
public required string Version { get; init; }
/// <summary>来源marketplace | local | github</summary>
public required string Source { get; init; }
/// <summary>主程序集路径</summary>
public required string AssemblyPath { get; init; }
/// <summary>是否启用</summary>
public bool IsEnabled { get; init; }
/// <summary>插件清单</summary>
public PluginManifest Manifest { get; init; } = new();
}
/// <summary>
/// 插件清单 — plugin.json 反序列化目标
/// 描述插件向宿主暴露的所有扩展点
/// </summary>
public sealed record PluginManifest
{
/// <summary>插件提供的技能列表</summary>
public IReadOnlyList<SkillDefinition> Skills { get; init; } = [];
/// <summary>插件提供的斜杠命令列表</summary>
public IReadOnlyList<ICommand> Commands { get; init; } = [];
/// <summary>插件注册的 MCP 服务器配置(服务器名 → 配置)</summary>
public IReadOnlyDictionary<string, ScopedMcpServerConfig> McpServers { get; init; }
= new Dictionary<string, ScopedMcpServerConfig>();
}
```
**设计意图**
`PluginManifest` 直接对应 `plugin.json` 的顶层结构三个扩展点技能、命令、MCP 服务器)分别映射为 `SkillLoader``CommandRegistry``McpClientManager` 的动态注册入口。插件卸载时,这三个注册表各自移除该插件贡献的条目。
---
## 17.3 PluginLoadContext
```csharp
/// <summary>
/// 可卸载的程序集加载上下文
/// 对应原始动态 import() 的隔离语义
/// </summary>
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<string, PluginLoadContext> _contexts = new();
private readonly ConcurrentDictionary<string, LoadedPlugin> _plugins = new();
public async Task<IReadOnlyList<LoadedPlugin>> 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<PluginManifest>(
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/<id>/
// 然后调用 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)