fengling-console/.planning/docs/gateway-plugin-system.md
movingsam bad0de9f3b
Some checks failed
Build and Push Docker / build (push) Failing after 5m9s
chore(docker): 替换基础镜像地址为私有仓库
- 将aspnet基础镜像地址替换为192.168.100.120私有仓库地址
- 将dotnet sdk镜像地址同样替换为私有仓库地址

docs(gateway): 补充YARP网关插件系统规划文档

- 增加整体架构图及说明,明确各项目职责
- 详细描述Web UI管理界面技术选型及功能模块
- 规范文档结构,提升可读性和完整性
2026-03-01 20:19:36 +08:00

19 KiB
Raw Blame History

网关插件系统技术方案

一、概述

本文档描述 YARP 网关的插件系统规划,包括 Web UI 管理界面和动态编译加载两大核心功能。


二、整体架构

┌─────────────────────┐
│ fengling-console   │  (运维后端 - Backend)
│      web 前端      │
└─────────┬───────────┘
          │ HTTP API
          ▼
┌─────────────────────┐
│ fengling-console   │  (运维服务端)
│                    │
│  - 路由管理 API    │ ───▶ 数据库持久化
│  - 集群管理 API    │ ───▶ Redis Pub/Sub (发布事件)
│  - 插件管理 API    │
└─────────┬───────────┘
          ▲
          │ 事件订阅
          │
┌─────────┴───────────┐
│ fengling-gateway    │  (YARP 网关多实例)
│  - YARP 代理       │
│  - 插件执行        │
│  - 事件监听        │
└─────────────────────┘

项目职责

项目 职责
fengling-gateway 纯 YARP 代理 + 事件订阅 + 插件执行
fengling-console 运维 API + 配置持久化 + 事件发布
fengling-console-web 前端 UI (Monaco Editor)

三、Web UI 管理界面

3.1 技术选型

项目 选择 理由
前端框架 React/Vue 独立前端项目
编辑器 Monaco Editor VS Code 同款,体验一致
路由 /gateway 运维平台内统一路由

3.2 功能模块

/gateway
├── 路由管理 (Routes)
│   ├── 列表/搜索
│   ├── 创建/编辑/删除
│   └── 路由规则配置
├── 集群管理 (Clusters)
│   ├── 上下游服务列表
│   ├── 实例管理
│   └── 健康状态
├── 插件管理 (Plugins)
│   ├── 已加载插件列表
│   ├── 上传 DLL
│   └── 在线编写 C# 代码
└── 监控统计
    ├── QPS/延迟
    └── 流量图表

一、概述

本文档描述 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 插件类型定义

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 插件阶段枚举

public enum PipelineStage
{
    None = 0,
    OnRequest = 1,          // 请求到达网关前
    OnRoute = 2,            // 路由决策时
    OnRequestBackend = 3,    // 转发到后端前
    OnResponseBackend = 4,   // 后端响应后
    OnResponse = 5           // 返回给客户端前
}

四、核心模块设计

4.1 依赖管理A方案

简单场景:直接使用网关已有程序集

// 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 包

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 上传验证

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强类型 + 弱类型混合

插件上下文

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!;
}

使用示例

// 插件 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-serverVS Code C# 扩展背后使用的语言服务器)。

部署方式

# 安装
dotnet tool install -g Microsoft.CodeAnalysis.LanguageServer

# 启动服务
roslyn-languageserver --port 5000

前端通过 WebSocket 连接该服务获取完整的语言特性支持。但目前阶段前端模拟补全已足够使用。

<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>

编辑器模板

// 生成的代码模板
$@"
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 插件生命周期管理

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 插件编译服务

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));
    }
}

五、数据库模型

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 依赖

<!-- 核心编译 -->
<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