diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml
index 8771701..1f133cf 100644
--- a/.gitea/workflows/docker.yml
+++ b/.gitea/workflows/docker.yml
@@ -16,8 +16,13 @@ jobs:
steps:
- uses: actions/checkout@v4
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ - name: Cache Docker layers
+ uses: actions/cache@v4
+ with:
+ path: /tmp/.buildx-cache
+ key: ${{ runner.os }}-buildx-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-buildx-
- name: Extract version
id: version
@@ -56,11 +61,12 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BUILD_VERSION=${{ steps.version.outputs.version }}
- cache-from: type=local,src=/data/.buildx-cache
- cache-to: type=local,dest=/data/.buildx-cache-new,mode=max
+ cache-from: type=local,src=/tmp/.buildx-cache
+ cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache
+ if: always()
run: |
- rm -rf /data/.buildx-cache
- mv /data/.buildx-cache-new /data/.buildx-cache
+ rm -rf /tmp/.buildx-cache
+ mv /tmp/.buildx-cache-new /tmp/.buildx-cache
diff --git a/.planning/docs/gateway-plugin-system.md b/.planning/docs/gateway-plugin-system.md
new file mode 100644
index 0000000..b610b53
--- /dev/null
+++ b/.planning/docs/gateway-plugin-system.md
@@ -0,0 +1,638 @@
+# 网关插件系统技术方案
+
+## 一、概述
+
+本文档描述 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
+{
+ ///
+ /// 插件基础接口
+ ///
+ public interface IGatewayPlugin
+ {
+ string Name { get; }
+ string Version { get; }
+ string? Description { get; }
+
+ Task OnLoadAsync();
+ Task OnUnloadAsync();
+ }
+
+ ///
+ /// 请求处理插件
+ ///
+ public interface IRequestPlugin : IGatewayPlugin
+ {
+ /// 请求到达网关前
+ Task OnRequestAsync(HttpContext context);
+
+ /// 路由决策后
+ Task OnRouteMatchedAsync(HttpContext context, RouteConfig route);
+
+ /// 转发到后端前
+ Task OnForwardingAsync(HttpContext context, HttpRequestMessage request);
+ }
+
+ ///
+ /// 响应处理插件
+ ///
+ public interface IResponsePlugin : IGatewayPlugin
+ {
+ /// 后端响应后
+ Task OnBackendResponseAsync(HttpContext context, HttpResponseMessage response);
+
+ /// 返回客户端前
+ Task OnResponseFinalizingAsync(HttpContext context);
+ }
+
+ ///
+ /// 路由转换插件
+ ///
+ public interface IRouteTransformPlugin : IGatewayPlugin
+ {
+ Task TransformRouteAsync(RouteConfig original, HttpContext context);
+ }
+
+ ///
+ /// 负载均衡插件
+ ///
+ public interface ILoadBalancePlugin : IGatewayPlugin
+ {
+ Task SelectDestinationAsync(
+ IReadOnlyList 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 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 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 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 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 _data = new();
+
+ public T? Get(string key) => _data.TryGetValue(key, out var v) ? (T)v : default;
+ public void Set(string key, T value) => _data[key] = value!;
+}
+```
+
+#### 使用示例
+
+```csharp
+// 插件 A: 认证
+public class AuthPlugin : IRequestPlugin
+{
+ public async Task 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 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
+
+
+
+
+
+```
+
+#### 编辑器模板
+
+```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 OnRequestAsync(HttpContext ctx)
+ {{
+ // 编写你的逻辑
+ {userCode}
+ }}
+}}
+";
+```
+
+---
+
+### 4.4 插件生命周期管理
+
+```csharp
+public class PluginManager
+{
+ private readonly Dictionary _plugins = new();
+ private readonly RoslynPluginCompiler _compiler = new();
+
+ public async Task 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 _defaultReferences;
+
+ public RoslynPluginCompiler()
+ {
+ _defaultReferences = GetDefaultReferences();
+ }
+
+ public CompileResult Compile(string sourceCode, string pluginName, IEnumerable 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 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
+
+
+
+
+
+
+
+
+```
+
+---
+
+## 八、总结
+
+本方案实现了:
+
+1. **Web UI 管理**:类 SwaggerUI 风格的可视化界面
+2. **动态编译**:Roslyn 在线编译 C# 代码
+3. **插件加载**:独立 AssemblyLoadContext,支持热卸载
+4. **灵活扩展**:支持简单场景(使用已有程序集)和复杂场景(上传 ZIP)
+5. **流程控制**:插件可分配到 5 个不同阶段执行
+
+---
+
+*文档版本: 1.0*
+*最后更新: 2026-03-01*