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

17 KiB
Raw Blame History

网关插件系统技术方案

一、概述

本文档描述 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