chore(docker): 优化 Docker 构建缓存管理
Some checks failed
Build and Push Docker / build (push) Failing after 31s

- 使用 actions/cache 缓存 Docker 构建层,加快构建速度
- 统一缓存路径改为 /tmp/.buildx-cache 以简化管理
- 更新构建缓存的来源和目标目录配置
- 确保构建结束时正确移动缓存文件,避免缓存丢失

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

- 详细描述插件系统的设计目标和核心功能
- 规划基于 Razor Pages 的 Web UI 管理界面
- 设计插件类型接口及生命周期管理方案
This commit is contained in:
movingsam 2026-03-01 16:38:10 +08:00
parent 9c59d6881b
commit bce6aaa33f
2 changed files with 650 additions and 6 deletions

View File

@ -16,8 +16,13 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Docker Buildx - name: Cache Docker layers
uses: docker/setup-buildx-action@v3 uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Extract version - name: Extract version
id: version id: version
@ -56,11 +61,12 @@ jobs:
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
BUILD_VERSION=${{ steps.version.outputs.version }} BUILD_VERSION=${{ steps.version.outputs.version }}
cache-from: type=local,src=/data/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/data/.buildx-cache-new,mode=max cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache - name: Move cache
if: always()
run: | run: |
rm -rf /data/.buildx-cache rm -rf /tmp/.buildx-cache
mv /data/.buildx-cache-new /data/.buildx-cache mv /tmp/.buildx-cache-new /tmp/.buildx-cache

View File

@ -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
{
/// <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*