fengling-console/.planning/docs/gateway-plugin-system.md
movingsam bce6aaa33f
Some checks failed
Build and Push Docker / build (push) Failing after 31s
chore(docker): 优化 Docker 构建缓存管理
- 使用 actions/cache 缓存 Docker 构建层,加快构建速度
- 统一缓存路径改为 /tmp/.buildx-cache 以简化管理
- 更新构建缓存的来源和目标目录配置
- 确保构建结束时正确移动缓存文件,避免缓存丢失

docs(gateway): 新增网关插件系统技术方案文档

- 详细描述插件系统的设计目标和核心功能
- 规划基于 Razor Pages 的 Web UI 管理界面
- 设计插件类型接口及生命周期管理方案
2026-03-01 16:38:10 +08:00

639 lines
17 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.

# 网关插件系统技术方案
## 一、概述
本文档描述 YARP 网关的插件系统规划,包括 Web UI 管理界面和动态编译加载两大核心功能。
---
## 二、Web UI 管理界面
### 2.1 技术选型
| 项目 | 选择 | 理由 |
|------|------|------|
| 框架 | Razor Pages | 嵌入主应用,单项目部署 |
| 路由 | `/gateway/ui` | 参考 SwaggerUI 风格 |
| 编辑器 | Monaco Editor | VS Code 同款,体验一致 |
### 2.2 功能模块
```
/gateway/ui
├── 路由管理 (Routes)
│ ├── 列表/搜索
│ ├── 创建/编辑/删除
│ └── 路由规则配置
├── 集群管理 (Clusters)
│ ├── 上下游服务列表
│ ├── 实例管理
│ └── 健康状态
├── 插件管理 (Plugins)
│ ├── 已加载插件列表
│ ├── 上传 DLL
│ └── 在线编写 C# 代码
└── 监控统计
├── QPS/延迟
└── 流量图表
```
---
## 三、插件系统架构
### 3.1 插件类型定义
```csharp
namespace Fengling.Gateway.Plugin.Abstractions
{
/// <summary>
/// 插件基础接口
/// </summary>
public interface IGatewayPlugin
{
string Name { get; }
string Version { get; }
string? Description { get; }
Task OnLoadAsync();
Task OnUnloadAsync();
}
/// <summary>
/// 请求处理插件
/// </summary>
public interface IRequestPlugin : IGatewayPlugin
{
/// <summary>请求到达网关前</summary>
Task<HttpContext?> OnRequestAsync(HttpContext context);
/// <summary>路由决策后</summary>
Task<HttpContext?> OnRouteMatchedAsync(HttpContext context, RouteConfig route);
/// <summary>转发到后端前</summary>
Task<HttpContext?> OnForwardingAsync(HttpContext context, HttpRequestMessage request);
}
/// <summary>
/// 响应处理插件
/// </summary>
public interface IResponsePlugin : IGatewayPlugin
{
/// <summary>后端响应后</summary>
Task OnBackendResponseAsync(HttpContext context, HttpResponseMessage response);
/// <summary>返回客户端前</summary>
Task OnResponseFinalizingAsync(HttpContext context);
}
/// <summary>
/// 路由转换插件
/// </summary>
public interface IRouteTransformPlugin : IGatewayPlugin
{
Task<RouteConfig> TransformRouteAsync(RouteConfig original, HttpContext context);
}
/// <summary>
/// 负载均衡插件
/// </summary>
public interface ILoadBalancePlugin : IGatewayPlugin
{
Task<Destination> SelectDestinationAsync(
IReadOnlyList<Destination> destinations,
HttpContext context);
}
}
```
### 3.2 插件阶段枚举
```csharp
public enum PipelineStage
{
None = 0,
OnRequest = 1, // 请求到达网关前
OnRoute = 2, // 路由决策时
OnRequestBackend = 3, // 转发到后端前
OnResponseBackend = 4, // 后端响应后
OnResponse = 5 // 返回给客户端前
}
```
---
## 四、核心模块设计
### 4.1 依赖管理A方案
#### 简单场景:直接使用网关已有程序集
```csharp
// API 暴露网关程序集
[ApiController]
public class AssembliesController : ControllerBase
{
[HttpGet("available")]
public List<AssemblyInfo> GetAvailableAssemblies()
{
return AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
.Select(a => new AssemblyInfo
{
Name = a.GetName().Name,
Version = a.GetName().Version?.ToString(),
Location = a.Location
})
.OrderBy(a => a.Name)
.ToList();
}
}
```
#### 复杂场景:上传 ZIP 包
```csharp
public class PluginUploadService
{
private readonly IObjectStorage _storage;
private readonly PluginDbContext _db;
private readonly string _localPluginPath = "/app/plugins";
public async Task<PluginPackage> UploadPluginAsync(IFormFile zipFile, string pluginName)
{
// 1. 上传到对象存储
var storageKey = $"plugins/{Guid.NewGuid()}/{zipFile.FileName}";
await _storage.UploadAsync(zipFile.OpenReadStream(), storageKey);
// 2. 保存到数据库
var plugin = new PluginPackage
{
Id = Guid.NewGuid(),
Name = pluginName,
StorageKey = storageKey,
UploadedAt = DateTime.UtcNow,
Status = PluginStatus.Pending
};
await _db.PluginPackages.AddAsync(plugin);
await _db.SaveChangesAsync();
return plugin;
}
public async Task ExtractAndLoadAsync(Guid pluginId)
{
var plugin = await _db.PluginPackages.FindAsync(pluginId);
// 3. 下载到本地
var localDir = Path.Combine(_localPluginPath, plugin.Id.ToString());
Directory.CreateDirectory(localDir);
await _storage.DownloadAsync(plugin.StorageKey, localDir + ".zip");
// 4. 解压
ZipFile.ExtractToDirectory(localDir + ".zip", localDir, overwriteFiles: true);
File.Delete(localDir + ".zip");
// 5. 加载插件
await _pluginManager.LoadFromDirectoryAsync(localDir);
}
}
```
#### ZIP 上传验证
```csharp
public class PluginValidationService
{
public async Task<PluginValidationResult> ValidateAsync(Stream zipStream)
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
await ExtractZipAsync(zipStream, tempDir);
try
{
var dlls = Directory.GetFiles(tempDir, "*.dll");
var result = new PluginValidationResult();
foreach (var dll in dlls)
{
var dllResult = await ValidateDllAsync(dll);
result.Assemblies.Add(dllResult);
}
var validPlugins = result.Assemblies
.Where(a => a.IsValidPlugin)
.ToList();
if (validPlugins.Count == 0)
{
result.IsValid = false;
result.ErrorMessage = "未找到实现 IGatewayPlugin 接口的类";
}
else
{
result.IsValid = true;
result.ValidPluginTypes = validPlugins
.SelectMany(a => a.PluginTypes)
.ToList();
}
return result;
}
finally
{
Directory.Delete(tempDir, true);
}
}
private async Task<DllValidationResult> ValidateDllAsync(string dllPath)
{
var result = new DllValidationResult { DllName = Path.GetFileName(dllPath) };
try
{
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(dllPath);
var pluginTypes = assembly.GetTypes()
.Where(t => typeof(IGatewayPlugin).IsAssignableFrom(t))
.Where(t => !t.IsAbstract && !t.IsInterface)
.ToList();
if (pluginTypes.Count > 0)
{
result.IsValidPlugin = true;
result.PluginTypes = pluginTypes.Select(t => new PluginTypeInfo
{
TypeName = t.FullName,
ImplementedInterfaces = t.GetInterfaces().Select(i => i.Name).ToList()
}).ToList();
}
}
catch (Exception ex)
{
result.IsValid = false;
result.ErrorMessage = ex.Message;
}
return result;
}
}
```
---
### 4.2 插件间通信B方案
采用**方案 3强类型 + 弱类型混合**
#### 插件上下文
```csharp
public class GatewayContext
{
// 预定义常用字段(强类型)
public string? UserId { get; set; }
public string? TenantId { get; set; }
public UserTier Tier { get; set; }
public bool IsAuthenticated { get; set; }
public DateTime RequestTime { get; set; }
// 扩展数据(弱类型)
public PluginDataBag Data { get; } = new();
}
public class PluginDataBag
{
private readonly Dictionary<string, object> _data = new();
public T? Get<T>(string key) => _data.TryGetValue(key, out var v) ? (T)v : default;
public void Set<T>(string key, T value) => _data[key] = value!;
}
```
#### 使用示例
```csharp
// 插件 A: 认证
public class AuthPlugin : IRequestPlugin
{
public async Task<HttpContext?> OnRequestAsync(HttpContext context)
{
var userId = ValidateToken(context);
if (userId != null)
{
context.Items["CurrentUserId"] = userId;
context.Items["IsAuthenticated"] = true;
}
return context;
}
}
// 插件 B: 审计
public class AuditPlugin : IRequestPlugin
{
public async Task<HttpContext?> OnRequestAsync(HttpContext context)
{
if (context.Items.TryGetValue("CurrentUserId", out var userId))
{
await _logger.LogAsync($"User {userId} accessed {context.Request.Path}");
}
return context;
}
}
```
---
### 4.3 在线代码编辑器C方案
采用 **Monaco Editor + 前端模拟补全**
> **补充说明**:如需更完整的 C# IntelliSense如真实代码分析、跳转到定义可使用 Microsoft 官方的 **roslyn-language-server**VS Code C# 扩展背后使用的语言服务器)。
>
> **部署方式**
> ```bash
> # 安装
> dotnet tool install -g Microsoft.CodeAnalysis.LanguageServer
>
> # 启动服务
> roslyn-languageserver --port 5000
> ```
>
> 前端通过 WebSocket 连接该服务获取完整的语言特性支持。但目前阶段前端模拟补全已足够使用。
```html
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
<div id="editor" style="height: 500px;"></div>
<script>
require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' }});
require(['vs/editor/editor.main'], function() {
monaco.editor.create(document.getElementById('editor'), {
value: getEditorTemplate(),
language: 'csharp',
theme: 'vs-dark',
automaticLayout: true,
minimap: { enabled: false },
fontSize: 14,
lineNumbers: 'on',
scrollBeyondLastLine: false
});
// 注册 C# 补全
monaco.languages.registerCompletionItemProvider('csharp', {
provideCompletionItems: (model, position) => {
const suggestions = [
// 常用类型
{ label: 'HttpContext', kind: monaco.languages.CompletionItemKind.Class },
{ label: 'HttpRequest', kind: monaco.languages.CompletionItemKind.Class },
{ label: 'HttpResponse', kind: monaco.languages.CompletionItemKind.Class },
// 插件接口方法
{ label: 'OnRequestAsync', kind: monaco.languages.CompletionItemKind.Method },
{ label: 'OnResponseAsync', kind: monaco.languages.CompletionItemKind.Method },
{ label: 'TransformRouteAsync', kind: monaco.languages.CompletionItemKind.Method },
// 常用属性
{ label: 'ctx.Request', kind: monaco.languages.CompletionItemKind.Property },
{ label: 'ctx.Response', kind: monaco.languages.CompletionItemKind.Property },
{ label: 'ctx.Items', kind: monaco.languages.CompletionItemKind.Property },
];
return { suggestions };
}
});
});
</script>
```
#### 编辑器模板
```csharp
// 生成的代码模板
$@"
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Fengling.Gateway.Plugin.Abstractions;
public class {pluginName} : IRequestPlugin
{{
public string Name => ""{pluginName}"";
public string Version => ""1.0.0"";
public async Task<HttpContext?> OnRequestAsync(HttpContext ctx)
{{
// 编写你的逻辑
{userCode}
}}
}}
";
```
---
### 4.4 插件生命周期管理
```csharp
public class PluginManager
{
private readonly Dictionary<string, PluginInstance> _plugins = new();
private readonly RoslynPluginCompiler _compiler = new();
public async Task<PluginInstance> LoadPluginAsync(byte[] assemblyBytes, string pluginName)
{
// 1. 隔离加载程序集
var context = new PluginLoadContext(pluginName);
var assembly = context.LoadFromStream(new MemoryStream(assemblyBytes));
// 2. 查找插件入口类型
var pluginType = assembly.GetTypes()
.FirstOrDefault(t => typeof(IGatewayPlugin).IsAssignableFrom(t));
if (pluginType == null)
{
context.Unload();
throw new InvalidOperationException("No IGatewayPlugin implementation found");
}
// 3. 创建实例
var plugin = (IGatewayPlugin)Activator.CreateInstance(pluginType)!;
await plugin.OnLoadAsync();
// 4. 保存实例
var instance = new PluginInstance
{
Name = pluginName,
Assembly = assembly,
Context = context,
Plugin = plugin,
LoadedAt = DateTime.UtcNow
};
_plugins[pluginName] = instance;
return instance;
}
public async Task UnloadPluginAsync(string pluginName)
{
if (!_plugins.TryGetValue(pluginName, out var instance))
return;
await instance.Plugin.OnUnloadAsync();
instance.Context.Unload();
_plugins.Remove(pluginName);
}
}
public class PluginLoadContext : AssemblyLoadContext
{
public PluginLoadContext(string name) : base(name, isCollectible: true) { }
protected override Assembly? Load(AssemblyName assemblyName)
{
return null;
}
}
```
---
### 4.5 插件编译服务
```csharp
public class RoslynPluginCompiler
{
private readonly IEnumerable<MetadataReference> _defaultReferences;
public RoslynPluginCompiler()
{
_defaultReferences = GetDefaultReferences();
}
public CompileResult Compile(string sourceCode, string pluginName, IEnumerable<string> extraAssemblies)
{
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
var references = _defaultReferences.Concat(
extraAssemblies.Select(a => MetadataReference.CreateFromFile(a))
);
var compilation = CSharpCompilation.Create(
assemblyName: $"Plugin_{pluginName}_{Guid.NewGuid():N}",
syntaxTrees: new[] { syntaxTree },
references: references,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithAllowUnsafe(false)
.WithOptimizationLevel(OptimizationLevel.Release));
using var ms = new MemoryStream();
var emitResult = compilation.Emit(ms);
if (!emitResult.Success)
{
return CompileResult.Fail(emitResult.Diagnostics);
}
ms.Seek(0, SeekOrigin.Begin);
return CompileResult.Success(ms.ToArray());
}
private IEnumerable<MetadataReference> GetDefaultReferences()
{
return AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
.Select(a => MetadataReference.CreateFromFile(a.Location));
}
}
```
---
## 五、数据库模型
```csharp
public class PluginPackage
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Version { get; set; } = "1.0.0";
public string? Description { get; set; }
public string StorageKey { get; set; } = string.Empty;
public PluginStatus Status { get; set; }
public DateTime UploadedAt { get; set; }
public DateTime? LoadedAt { get; set; }
}
public class PluginPipeline
{
public Guid Id { get; set; }
public Guid PluginId { get; set; }
public PipelineStage Stage { get; set; }
public int Order { get; set; }
public bool IsEnabled { get; set; }
}
public enum PluginStatus
{
Pending = 0,
Validated = 1,
Loaded = 2,
Failed = 3,
Disabled = 4
}
```
---
## 六、实施计划
| 阶段 | 任务 | 优先级 |
|------|------|--------|
| Phase 1 | 集成 YARP 到现有项目 | 高 |
| Phase 2 | 插件基础接口定义 | 高 |
| Phase 3 | 插件编译 + 加载框架 | 高 |
| Phase 4 | 嵌入式 Razor UI | 中 |
| Phase 5 | Monaco Editor 集成 | 中 |
| Phase 6 | ZIP 上传验证功能 | 中 |
| Phase 7 | 测试与优化 | 低 |
---
## 七、NuGet 依赖
```xml
<!-- 核心编译 -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
<!-- Scripting API (可选) -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.13.0" />
<!-- 依赖解析 -->
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
```
---
## 八、总结
本方案实现了:
1. **Web UI 管理**:类 SwaggerUI 风格的可视化界面
2. **动态编译**Roslyn 在线编译 C# 代码
3. **插件加载**:独立 AssemblyLoadContext支持热卸载
4. **灵活扩展**:支持简单场景(使用已有程序集)和复杂场景(上传 ZIP
5. **流程控制**:插件可分配到 5 个不同阶段执行
---
*文档版本: 1.0*
*最后更新: 2026-03-01*