diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 3d5a64d..46b51ce 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -1,4 +1,4 @@
-# Roadmap:Fengling Gateway
+#MY|# Roadmap:Fengling Gateway
**创建日期:** 2026-03-02
**核心价值:** 可靠、可扩展的 API 网关,将流量分发到后端微服务,支持零停机配置更新。
@@ -100,35 +100,99 @@
1. 分布式追踪包含网关跨度
2. 导出关键指标(请求数、延迟、错误率)
3. 核心组件测试覆盖率 >80%
-## 阶段 6:网关插件技术调研与实现
+
+---
+
+BZ|## 阶段 6:网关插件技术调研与实现 ✅ 进行中
+
+**目标:** 实现网关插件化支持。
+
+**已规划计划:**
+- 006-01: ✅ 已完成 - 插件加载基础设施 (PLUG-01, PLUG-02)
+- 006-02: 📋 待执行 - YARP 插件集成 (PLUG-03)
+
+**需求:**
+- [x] PLUG-01:网关插件化架构设计
+- [x] PLUG-02:插件加载机制
+- [ ] PLUG-03:YARP 插件集成
+
+**成功标准:**
+- [x] 1. 网关支持动态加载插件
+- [x] 2. 插件之间相互隔离
+- [ ] 3. 插件可以在运行时热加载/卸载
+
+**已实现文件 (006-01):**
+- `src/yarpgateway/Plugins/PluginLoadContext.cs` - ALC 隔离
+- `src/yarpgateway/Plugins/PluginLoader.cs` - 发现和加载
+- `src/yarpgateway/Plugins/PluginHost.cs` - 生命周期管理
+- 单元测试 15 个全部通过
+
+**待实现文件 (006-02):**
+- `src/yarpgateway/Plugins/PluginTransformProvider.cs` - YARP Transform 提供者
+- `src/yarpgateway/Plugins/DestinationSelector.cs` - 目标选择器
+- `src/yarpgateway/Plugins/PluginConfigWatcher.cs` - Console DB 通知监听
**目标:** 实现网关插件化支持。
**需求:**
-- [ ] PLUG-01:网关插件化架构设计
-- [ ] PLUG-02:插件加载机制
+- [x] PLUG-01:网关插件化架构设计
+- [x] PLUG-02:插件加载机制
- [ ] PLUG-03:插件隔离与生命周期管理
**成功标准:**
-1. 网关支持动态加载插件
-2. 插件之间相互隔离
-3. 插件可以在运行时热加载/卸载
+- [x] 1. 网关支持动态加载插件
+- [x] 2. 插件之间相互隔离
+- [ ] 3. 插件可以在运行时热加载/卸载
+
+**已实现文件:**
+- `src/yarpgateway/Plugins/PluginLoadContext.cs` - ALC 隔离
+- `src/yarpgateway/Plugins/PluginLoader.cs` - 发现和加载
+- `src/yarpgateway/Plugins/PluginHost.cs` - 生命周期管理
+- 单元测试 15 个全部通过
---
## Roadmap 摘要
-|| 阶段 | 名称 | 需求数 | 状态 |
-||------|------|--------|------|
-|| 1 | 配置变更监听与多实例支持 | 6 | ✅ 已完成 |
-|| 2 | K8s 健康检查委托 | 2 | ✅ 已完成 |
-|| 3 | 安全加固 | 3 | 未规划 |
-|| 4 | 性能优化 | 2 | 未规划 |
-|| 5 | 可观测性与测试 | 5 | 未规划 |
-|| 6 | 网关插件技术调研与实现 | 3 | 未规划 |
+| 阶段 | 名称 | 需求数 | 状态 |
+|------|------|--------|------|
+| 1 | 配置变更监听与多实例支持 | 6 | ✅ 已完成 |
+| 2 | K8s 健康检查委托 | 2 | ✅ 已完成 |
+| 3 | 安全加固 | 3 | 未规划 |
+| 4 | 性能优化 | 2 | 未规划 |
+| 5 | 可观测性与测试 | 5 | 未规划 |
+| 6 | 网关插件技术调研与实现 | 3 | ✅ 进行中 |
-**总计:** 6 个阶段 | 21 个需求 | 8 项已完成
+## 阶段 7:网关配置重构规划
+
+**目标:** 分析网关配置新想法的可行性,重新规划网关配置架构。
+
+**需求:**
+- [ ] REPL-01:分析"网关配置的新想法.md"中的方案
+- [ ] REPL-02:识别与现有需求的冲突点
+- [ ] REPL-03:制定新的网关配置架构
+
+**成功标准:**
+1. 完成网关配置新想法的可行性分析
+2. 提出与现有阶段兼容的配置方案
+3. 更新 roadmap 以反映新的配置架构
---
-*最后更新:2026-03-04 阶段6已添加*
+## Roadmap 摘要
+
+| 阶段 | 名称 | 需求数 | 状态 |
+|------|------|--------|------|
+| 1 | 配置变更监听与多实例支持 | 6 | ✅ 已完成 |
+| 2 | K8s 健康检查委托 | 2 | ✅ 已完成 |
+| 3 | 安全加固 | 3 | 未规划 |
+| 4 | 性能优化 | 2 | 未规划 |
+| 5 | 可观测性与测试 | 5 | 未规划 |
+| 6 | 网关插件技术调研与实现 | 3 | ✅ 进行中 |
+| 7 | 网关配置重构规划 | 3 | 待规划 |
+
+**总计:** 7 个阶段 | 24 个需求 | 8 项已完成
+
+---
+
+*最后更新:2026-03-04 阶段6已完成 PLUG-01 和 PLUG-02*
diff --git a/.planning/STATE.md b/.planning/STATE.md
index 12ae3d0..5c861c8 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -1,6 +1,6 @@
-# 状态:Fengling Gateway
+#VR|# 状态:Fengling Gateway
-**最后更新:** 2026-03-02
+**最后更新:** 2026-03-04
---
@@ -10,7 +10,7 @@
**核心价值:** 可靠、可扩展的 API 网关,将流量分发到后端微服务,支持零停机配置更新。
-**当前重点:** 阶段 2:K8s 健康检查委托
+**当前重点:** 阶段 6:网关插件技术调研与实现
---
@@ -21,7 +21,7 @@
| PROJECT.md | ✓ 已初始化 |
| config.json | ✓ 已创建 |
| 需求文档 | ✓ 已定义(18 个需求) |
-| Roadmap | ✓ 已创建(5 个阶段) |
+| Roadmap | ✓ 已创建(6 个阶段) |
| 研究 | 未开始(自动模式跳过) |
---
@@ -31,10 +31,12 @@
| 阶段 | 名称 | 状态 | 计划数 | 进度 |
|------|------|------|--------|------|
| 1 | 配置变更监听与多实例支持 | ✅ 已完成 | 0 | 100% |
-| 2 | K8s 健康检查委托 | 未规划 | 0 | 0% |
+| 2 | K8s 健康检查委托 | ✅ 已完成 | 0 | 100% |
| 3 | 安全加固 | 未规划 | 0 | 0% |
| 4 | 性能优化 | 未规划 | 0 | 0% |
| 5 | 可观测性与测试 | 未规划 | 0 | 0% |
+#NH|QJ|| 6 | 网关插件技术调研与实现 | ✅ 进行中 | 2 | 50% |
+#PW|| 7 | 网关配置重构规划 | 待规划 | 0 | 0% |
---
@@ -67,31 +69,60 @@
- Console 拥有 GatewayDbContext,可直接管理网关配置
- ReloadGatewayAsync() 为空实现,需要在 fengling-console 中实现 NOTIFY 发送
-### Roadmap 演进
+SK|### 阶段 6 实施
-- **2026-03-02:** 阶段 1 需求分析完成 - 无需额外代码
-- 阶段 1 已完成:配置变更监听与多实例支持
-- 阶段 2 待添加:K8s 健康检查委托
-- 阶段 3 待添加:安全加固
-- 阶段 4 待添加:性能优化
-- 阶段 5 待添加:可观测性与测试
-- 阶段 6 已添加:网关插件技术调研与实现
+**2026-03-04:** 实现插件加载基础设施
+- 完成 PLUG-01:网关插件化架构设计
+- 完成 PLUG-02:插件加载机制
+- 实现文件:
+ - `src/yarpgateway/Plugins/PluginLoadContext.cs`
+ - `src/yarpgateway/Plugins/PluginLoader.cs`
+ - `src/yarpgateway/Plugins/PluginHost.cs`
+- 单元测试:15 个全部通过
+
+**2026-03-04:** YARP 集成设计决策(与用户讨论)
+- 管道选择:Transforms(更轻量,少资源占用)
+- OnRouteMatchedAsync:用于目标选择(特殊租户访问特殊目标)
+- 负载均衡:暂不考虑,使用内置
+- 插件启用:Metadata + Console DB 通知触发
+- 插件排序:需要(通过 PluginOrder metadata)
+
+**已规划计划 006-02:**
+- PluginTransformProvider - YARP Transform 提供者
+- DestinationSelector - 目标选择器
+- PluginConfigWatcher - Console DB 通知监听
+
+XX|---
+
+
+
+- **2026-03-04:** 实现插件加载基础设施
+- 完成 PLUG-01:网关插件化架构设计
+- 完成 PLUG-02:插件加载机制
+- 实现文件:
+ - `src/yarpgateway/Plugins/PluginLoadContext.cs`
+ - `src/yarpgateway/Plugins/PluginLoader.cs`
+ - `src/yarpgateway/Plugins/PluginHost.cs`
+- 单元测试:15 个全部通过
+
+### Roadmap Evolution
+
+- **2026-03-07:** 阶段 7 添加:网关配置重构规划(分析"网关配置的新想法.md"中的方案,识别冲突点,制定新架构)
+
+## 快速任务完成
+
+| # | Description | Date | Commit | Directory |
+|---|-------------|------|--------|----------|
+| 001 | 升级 Fengling.Platform 包并修复编译警告 | 2026-03-04 | 42b8c9c | [001-upgrade-platform](./quick/001-upgrade-platform/) |
---
-#SX|## 快速任务完成
-#KB|
-#KB|| # | Description | Date | Commit | Directory |
-#KB||---|-------------|------|--------|----------|
-#KB|| 001 | 升级 Fengling.Platform 包并修复编译警告 | 2026-03-04 | 42b8c9c | [001-upgrade-platform](./quick/001-upgrade-platform/) |
-#KB|
-#KB|---
-#KB|
-#KB|## 备注
-#KB|
-#KB|- 自动模式:跳过研究,工作流偏好设置为 yolo
-#KB|- 配置变更应提交到 git(commit_docs: true)
-#KB|- gsd-tools.cjs 不可用 - 项目结构手动创建
-#KB|
-#KB|---
-#KB|
-#KB|*最后更新:2026-03-04 - 完成快速任务 001: 升级 Fengling.Platform 包并修复编译警告*
+
+## 备注
+
+- 自动模式:跳过研究,工作流偏好设置为 yolo
+- 配置变更应提交到 git(commit_docs: true)
+- gsd-tools.cjs 不可用 - 项目结构手动创建
+
+---
+
+*最后更新:2026-03-04 - 完成阶段 6 计划 006-01:插件加载基础设施(PLUG-01, PLUG-02)*
diff --git a/.planning/phases/006-gateway-plugin-research/006-01-PLAN.md b/.planning/phases/006-gateway-plugin-research/006-01-PLAN.md
new file mode 100644
index 0000000..f67b984
--- /dev/null
+++ b/.planning/phases/006-gateway-plugin-research/006-01-PLAN.md
@@ -0,0 +1,290 @@
+---
+phase: 06-gateway-plugin-research
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified: []
+autonomous: true
+requirements: [PLUG-01, PLUG-02]
+must_haves:
+ truths:
+ - "插件可以从指定目录动态加载"
+ - "插件在独立的 AssemblyLoadContext 中运行"
+ - "插件可以被卸载并释放内存"
+ artifacts:
+ - path: "src/yarpgateway/Plugins/PluginLoadContext.cs"
+ provides: "ALC 隔离机制"
+ - path: "src/yarpgateway/Plugins/PluginLoader.cs"
+ provides: "插件发现和加载"
+ - path: "src/yarpgateway/Plugins/PluginHost.cs"
+ provides: "插件生命周期管理"
+ - path: "tests/YarpGateway.Tests/Unit/Plugins/PluginLoadTests.cs"
+ provides: "加载/卸载验证"
+ key_links:
+ - from: "PluginLoader"
+ to: "PluginLoadContext"
+ via: "实例化并加载程序集"
+ - from: "PluginHost"
+ to: "PluginLoader"
+ via: "协调插件生命周期"
+---
+
+# 计划 01:插件加载基础设施
+
+
+实现插件动态加载基础设施,包括 AssemblyLoadContext 隔离、插件发现和生命周期管理。
+
+**目的:** 为网关提供安全的插件加载机制,支持隔离和热重载。
+**产出:** 可工作的插件加载系统,含单元测试。
+
+
+
+@/Users/mac/.config/opencode/get-shit-done/workflows/execute-plan.md
+@/Users/mac/.config/opencode/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/006-gateway-plugin-research/006-RESEARCH.md
+
+# 现有基础设施
+@src/Fengling.Gateway.Plugin.Abstractions/IGatewayPlugin.cs
+@src/yarpgateway/Program.cs
+
+
+
+
+```csharp
+// Fengling.Gateway.Plugin.Abstractions
+public interface IGatewayPlugin
+{
+ string Name { get; }
+ string Version { get; }
+ string? Description { get; }
+ Task OnLoadAsync();
+ Task OnUnloadAsync();
+}
+```
+
+
+
+
+
+ Task 1: 创建 PluginLoadContext 隔离机制
+
+ src/yarpgateway/Plugins/PluginLoadContext.cs,
+ tests/YarpGateway.Tests/Unit/Plugins/PluginLoadContextTests.cs
+
+
+ - Test 1: 加载插件程序集到独立 ALC
+ - Test 2: 共享契约程序集使用默认 ALC
+ - Test 3: 卸载 ALC 后内存被回收
+
+
+创建可卸载的 AssemblyLoadContext:
+
+1. 创建 `src/yarpgateway/Plugins/PluginLoadContext.cs`
+2. 继承 AssemblyLoadContext,设置 isCollectible: true
+3. 使用 AssemblyDependencyResolver 解析依赖
+4. 关键:共享 `Fengling.Gateway.Plugin.Abstractions` 到默认 ALC
+
+```csharp
+public sealed class PluginLoadContext : AssemblyLoadContext
+{
+ private readonly AssemblyDependencyResolver _resolver;
+ private readonly string _sharedAssemblyName = "Fengling.Gateway.Plugin.Abstractions";
+
+ public PluginLoadContext(string pluginPath) : base(isCollectible: true)
+ {
+ _resolver = new AssemblyDependencyResolver(pluginPath);
+ }
+
+ protected override Assembly? Load(AssemblyName assemblyName)
+ {
+ if (assemblyName.Name == _sharedAssemblyName)
+ return null; // 使用默认 ALC
+ var path = _resolver.ResolveAssemblyToPath(assemblyName);
+ return path != null ? LoadFromAssemblyPath(path) : null;
+ }
+
+ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
+ {
+ var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
+ return path != null ? LoadUnmanagedDllFromPath(path) : IntPtr.Zero;
+ }
+}
+```
+
+**注意**:先写测试,确保卸载验证通过。
+
+
+ dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoadContextTests" --no-build
+
+
+ - PluginLoadContext 类存在
+ - 测试验证隔离和卸载
+ - 构建通过
+
+
+
+
+ Task 2: 创建 PluginLoader 发现和加载逻辑
+
+ src/yarpgateway/Plugins/PluginLoader.cs,
+ src/yarpgateway/Plugins/DiscoveredPlugin.cs,
+ tests/YarpGateway.Tests/Unit/Plugins/PluginLoaderTests.cs
+
+
+ - Test 1: 从目录发现插件程序集
+ - Test 2: 加载插件并返回 IGatewayPlugin 实例
+ - Test 3: 处理无效插件(返回 null 或异常)
+
+
+创建插件发现和加载器:
+
+1. 创建 `DiscoveredPlugin.cs` 记录:
+ - Id, Name, Version, AssemblyPath, EntryPoint
+
+2. 创建 `PluginLoader.cs`:
+ - `DiscoverPlugins(string directory)` - 扫描目录发现插件
+ - `LoadPlugin(DiscoveredPlugin discovered)` - 加载并实例化
+ - 使用 System.Reflection.Metadata 或简单扫描
+
+```csharp
+public class PluginLoader
+{
+ public IEnumerable DiscoverPlugins(string pluginDirectory)
+ {
+ // 扫描 plugin.json 或程序集属性
+ }
+
+ public IGatewayPlugin? LoadPlugin(DiscoveredPlugin discovered)
+ {
+ var shadowPath = CreateShadowCopy(discovered.AssemblyPath);
+ var alc = new PluginLoadContext(shadowPath);
+ var assembly = alc.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(discovered.AssemblyPath)));
+ var type = assembly.GetType(discovered.EntryPoint);
+ return Activator.CreateInstance(type) as IGatewayPlugin;
+ }
+}
+```
+
+**影子复制**:创建 `CreateShadowCopy` 方法,将插件复制到临时目录。
+
+
+ dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoaderTests" --no-build
+
+
+ - PluginLoader 类存在
+ - 可发现和加载插件
+ - 测试通过
+
+
+
+
+ Task 3: 创建 PluginHost 生命周期管理
+
+ src/yarpgateway/Plugins/PluginHost.cs,
+ src/yarpgateway/Plugins/PluginHandle.cs,
+ tests/YarpGateway.Tests/Unit/Plugins/PluginHostTests.cs
+
+
+ - Test 1: LoadAllAsync 加载目录下所有插件
+ - Test 2: UnloadAsync 卸载指定插件
+ - Test 3: GetPlugins 返回当前加载的插件
+ - Test 4: 插件卸载后 WeakReference 显示已回收
+
+
+创建插件生命周期管理器:
+
+1. 创建 `PluginHandle.cs`:
+ - 封装 PluginLoadContext 和 IGatewayPlugin
+ - 实现 IAsyncDisposable
+ - 提供 TrackUnloadability() 返回 WeakReference
+
+```csharp
+public sealed class PluginHandle : IAsyncDisposable
+{
+ private readonly PluginLoadContext _alc;
+ private readonly IGatewayPlugin _plugin;
+ private readonly string _shadowDirectory;
+ private bool _disposed;
+
+ public IGatewayPlugin Plugin => _plugin;
+ public WeakReference TrackUnloadability() => new(_alc, trackResurrection: true);
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ await _plugin.OnUnloadAsync();
+ _alc.Unload();
+ }
+}
+```
+
+2. 创建 `PluginHost.cs`:
+ - 注入 PluginLoader
+ - 管理插件字典 (name -> PluginHandle)
+ - 提供 LoadAllAsync, UnloadAsync, GetPlugins
+
+```csharp
+public class PluginHost
+{
+ private readonly ConcurrentDictionary _plugins = new();
+ private readonly PluginLoader _loader;
+
+ public async Task LoadAllAsync(string pluginDirectory, CancellationToken ct = default)
+ {
+ var discovered = _loader.DiscoverPlugins(pluginDirectory);
+ foreach (var d in discovered)
+ {
+ var handle = _loader.LoadPlugin(d);
+ if (handle != null)
+ {
+ await handle.Plugin.OnLoadAsync();
+ _plugins[d.Id] = handle;
+ }
+ }
+ }
+
+ public async Task UnloadAsync(string pluginId)
+ {
+ if (_plugins.TryRemove(pluginId, out var handle))
+ {
+ await handle.DisposeAsync();
+ }
+ }
+}
+```
+
+
+ dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginHostTests" --no-build
+
+
+ - PluginHost 和 PluginHandle 类存在
+ - 可加载/卸载插件
+ - 卸载验证测试通过
+
+
+
+
+
+
+1. dotnet build src/yarpgateway/YarpGateway.csproj 无错误
+2. dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoad" 通过
+3. 插件加载/卸载功能可用
+
+
+
+- 插件可在独立 AssemblyLoadContext 中加载
+- 插件可通过 WeakReference 验证卸载
+- 所有单元测试通过
+
+
+
\ No newline at end of file
diff --git a/.planning/phases/006-gateway-plugin-research/006-01-SUMMARY.md b/.planning/phases/006-gateway-plugin-research/006-01-SUMMARY.md
new file mode 100644
index 0000000..72d7f40
--- /dev/null
+++ b/.planning/phases/006-gateway-plugin-research/006-01-SUMMARY.md
@@ -0,0 +1,57 @@
+# 计划 006-01 总结:插件加载基础设施
+
+## 执行状态:✅ 已完成
+
+## 完成的任务
+
+### Task 1: PluginLoadContext 隔离机制
+- ✅ 创建 `src/yarpgateway/Plugins/PluginLoadContext.cs`
+- ✅ 实现可卸载的 AssemblyLoadContext
+- ✅ 支持共享契约程序集(使用默认 ALC)
+- ✅ 创建单元测试 `PluginLoadContextTests.cs`
+
+### Task 2: PluginLoader 发现和加载逻辑
+- ✅ 创建 `src/yarpgateway/Plugins/PluginLoader.cs`
+- ✅ 实现插件发现(从目录扫描 plugin.json)
+- ✅ 实现影子复制(支持热重载)
+- ✅ 创建 `DiscoveredPlugin.cs` 和 `PluginMetadata.cs`
+- ✅ 创建单元测试 `PluginLoaderTests.cs`
+
+### Task 3: PluginHost 生命周期管理
+- ✅ 创建 `src/yarpgateway/Plugins/PluginHost.cs`
+- ✅ 实现 `PluginHandle.cs` 封装 ALC 和插件实例
+- ✅ 支持加载/卸载/重载插件
+- ✅ 提供 WeakReference 卸载验证
+- ✅ 创建单元测试 `PluginHostTests.cs`
+
+## 实现的文件
+
+| 文件 | 描述 |
+|------|------|
+| `src/yarpgateway/Plugins/PluginLoadContext.cs` | ALC 隔离机制 |
+| `src/yarpgateway/Plugins/PluginLoader.cs` | 插件发现和加载 |
+| `src/yarpgateway/Plugins/PluginHost.cs` | 生命周期管理 |
+| `tests/YarpGateway.Tests/Unit/Plugins/PluginLoadContextTests.cs` | 隔离测试 |
+| `tests/YarpGateway.Tests/Unit/Plugins/PluginLoaderTests.cs` | 加载测试 |
+| `tests/YarpGateway.Tests/Unit/Plugins/PluginHostTests.cs` | 生命周期测试 |
+
+## 测试结果
+
+```
+dotnet test --filter "FullyQualifiedName~Plugin"
+已通过! - 失败: 0,通过: 15,总计: 15
+```
+
+## 验证
+
+- ✅ 构建通过:`dotnet build src/yarpgateway/YarpGateway.csproj` - 0 错误
+- ✅ 所有单元测试通过
+- ✅ 插件可在独立 AssemblyLoadContext 中加载
+- ✅ 插件可通过 WeakReference 验证卸载
+- ✅ 支持动态发现和加载插件
+
+## 后续计划
+
+阶段 6 还需完成:
+- PLUG-03:插件隔离与生命周期管理(已实现基础设施)
+- YARP 集成:将插件集成到 YARP 管道
diff --git a/.planning/phases/006-gateway-plugin-research/006-02-PLAN.md b/.planning/phases/006-gateway-plugin-research/006-02-PLAN.md
new file mode 100644
index 0000000..8be44c8
--- /dev/null
+++ b/.planning/phases/006-gateway-plugin-research/006-02-PLAN.md
@@ -0,0 +1,366 @@
+---
+phase: 06-gateway-plugin-research
+plan: 02
+type: execute
+wave: 1
+depends_on: [006-01]
+files_modified: []
+autonomous: true
+requirements: [PLUG-03]
+must_haves:
+ truths:
+ - "插件通过路由 Metadata 启用"
+ - "请求 Transform 轻量处理请求"
+ - "目标选择在路由匹配后执行"
+ - "插件按配置顺序执行"
+ artifacts:
+ - path: "src/yarpgateway/Plugins/PluginTransformProvider.cs"
+ provides: "YARP Transform 提供者"
+ - path: "src/yarpgateway/Plugins/YarpPluginMiddleware.cs"
+ provides: "插件管道集成"
+ - path: "src/yarpgateway/Plugins/PluginConfigWatcher.cs"
+ provides: "Console DB 通知监听"
+ - path: "tests/YarpGateway.Tests/Unit/Plugins/YarpIntegrationTests.cs"
+ provides: "集成测试"
+key_links:
+ - from: "PluginTransformProvider"
+ to: "PluginHost"
+ via: "获取已加载插件"
+ - from: "YarpPluginMiddleware"
+ to: "PluginTransformProvider"
+ via: "应用 Transform"
+ - from: "PluginConfigWatcher"
+ to: "PluginHost"
+ via: "触发重载"
+---
+
+# 计划 02:YARP 插件集成
+
+
+将插件系统集成到 YARP 反向代理管道,实现 Transform 方式的请求/响应处理,以及通过 Metadata 驱动的插件启用机制。
+
+**目的:** 实现 PLUG-03 - 插件隔离与生命周期管理,完成网关插件化。
+**产出:** 可工作的 YARP 插件集成系统,含单元测试。
+
+
+
+@/Users/mac/.config/opencode/get-shit-done/workflows/execute-plan.md
+@/Users/mac/.config/opencode/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/006-gateway-plugin-research/006-RESEARCH.md
+@src/Fengling.Gateway.Plugin.Abstractions/IGatewayPlugin.cs
+@src/yarpgateway/Plugins/PluginHost.cs
+@src/yarpgateway/Program.cs
+
+
+
+## 现有插件接口
+
+```csharp
+// 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);
+}
+```
+
+## YARP Transform 接口
+
+```csharp
+// YARP Transform
+public interface IRequestTransform
+{
+ Task ApplyAsync(RequestTransformContext context);
+}
+
+public interface IResponseTransform
+{
+ Task ApplyAsync(ResponseTransformContext context);
+}
+
+public interface IClusterDestinationsTransform
+{
+ Task ApplyAsync(ClusterDestinationsContext context);
+}
+```
+
+
+
+
+
+ Task 1: 创建 PluginTransformProvider
+
+ src/yarpgateway/Plugins/PluginTransformProvider.cs,
+ tests/YarpGateway.Tests/Unit/Plugins/PluginTransformProviderTests.cs
+
+
+ - Test 1: 从路由 Metadata 发现启用的插件
+ - Test 2: 按 PluginOrder 排序
+ - Test 3: 创建 RequestTransform
+ - Test 4: 创建 ResponseTransform
+
+
+创建 YARP Transform 提供者:
+
+1. 创建 `PluginTransformProvider.cs`:
+ - 实现 `IProxyConfigFilter` 或 `IDynamicRouteConfigProvider`
+ - 扫描路由 Metadata 中的 "Plugins" 键
+ - 从 PluginHost 获取已加载的插件
+ - 按 "PluginOrder" 排序
+
+2. 核心逻辑:
+```csharp
+public class PluginTransformProvider : IProxyConfigFilter
+{
+ private readonly PluginHost _pluginHost;
+
+ public async Task ApplyTransformAsync(HttpContext context, RouteConfig route)
+ {
+ var pluginIds = route.Metadata?["Plugins"]?.Split(',');
+ if (pluginIds == null) return;
+
+ var order = route.Metadata?["PluginOrder"]?.Split(',') ?? pluginIds;
+ var orderedPlugins = order.Select(id => pluginIds.IndexOf(id))
+ .Select(i => _pluginHost.GetPlugins().ElementAt(i));
+
+ foreach (var plugin in orderedPlugins.OfType())
+ {
+ await plugin.OnRouteMatchedAsync(context, route);
+ }
+ }
+}
+```
+
+3. 创建 Request/Response Transform 类:
+ - `PluginRequestTransform` - 调用 IRequestPlugin
+ - `PluginResponseTransform` - 调用 IResponsePlugin
+ - `PluginRouteTransform` - 调用 IRouteTransformPlugin
+
+
+ dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginTransformProviderTests" --no-build
+
+
+ - PluginTransformProvider 存在
+ - Transform 测试通过
+
+
+
+
+ Task 2: 创建目标选择器 (DestinationSelector)
+
+ src/yarpgateway/Plugins/DestinationSelector.cs,
+ tests/YarpGateway.Tests/Unit/Plugins/DestinationSelectorTests.cs
+
+
+ - Test 1: OnRouteMatchedAsync 选择目标
+ - Test 2: 根据上下文修改目标列表
+ - Test 3: 特殊租户路由到特殊目标
+
+
+创建目标选择器,用于 OnRouteMatchedAsync 阶段:
+
+1. 创建 `DestinationSelector.cs`:
+ - 实现 IClusterDestinationsTransform
+ - 在路由匹配后、负载均衡前执行
+ - 可以根据 HttpContext 修改可用目标列表
+
+```csharp
+public class DestinationSelector : IClusterDestinationsTransform
+{
+ private readonly PluginHost _pluginHost;
+
+ public async Task ApplyAsync(ClusterDestinationsContext context)
+ {
+ var httpContext = context.HttpContext;
+
+ // 获取路由上启用的插件
+ var routeConfig = httpContext.GetRouteConfig();
+ var pluginIds = routeConfig?.Metadata?["Plugins"]?.Split(',');
+
+ if (pluginIds == null) return;
+
+ foreach (var pluginId in pluginIds)
+ {
+ var plugin = _pluginHost.GetPlugin(pluginId);
+ if (plugin is IRequestPlugin requestPlugin)
+ {
+ // 让插件过滤/修改目标列表
+ var availableDestinations = context.Destinations.ToList();
+ // 插件逻辑可以修改 availableDestinations
+ context.Destinations = availableDestinations;
+ }
+ }
+ }
+}
+```
+
+2. 注册到 YARP:
+```csharp
+builder.Services.AddSingleton();
+```
+
+
+ dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~DestinationSelectorTests" --no-build
+
+
+ - DestinationSelector 存在
+ - 目标选择逻辑工作正常
+
+
+
+
+ Task 3: 创建 PluginConfigWatcher (Console DB 通知)
+
+ src/yarpgateway/Plugins/PluginConfigWatcher.cs,
+ tests/YarpGateway.Tests/Unit/Plugins/PluginConfigWatcherTests.cs
+
+
+ - Test 1: 监听配置变更通知
+ - Test 2: 触发插件重载
+ - Test 3: 处理通知失败
+
+
+创建配置监听器,监听 Console DB 通知:
+
+1. 创建 `PluginConfigWatcher.cs`:
+ - 实现 `IHostedService` 或使用现有的 `PgSqlConfigChangeListener`
+ - 监听插件配置变更频道(如 `plugin_config_changed`)
+ - 触发 PluginHost 重载
+
+```csharp
+public class PluginConfigWatcher : BackgroundService
+{
+ private readonly PluginHost _pluginHost;
+ private readonly NpgsqlConnection _connection;
+ private const string Channel = "plugin_config_changed";
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ await _connection.OpenAsync(stoppingToken);
+ await _connection.ListenAsync(Channel, stoppingToken);
+
+ await foreach (var notification in _connection.Notifications(stoppingToken))
+ {
+ // 解析通知,获取需要重载的插件
+ var payload = JsonSerializer.Deserialize(notification.Payload);
+ await _pluginHost.ReloadAsync(payload.PluginId);
+ }
+ }
+}
+```
+
+2. 注册到 DI:
+```csharp
+builder.Services.AddHostedService();
+```
+
+
+ dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginConfigWatcherTests" --no-build
+
+
+ - PluginConfigWatcher 存在
+ - 监听器正确响应通知
+
+
+
+
+ Task 4: 更新 Program.cs 集成插件系统
+
+ src/yarpgateway/Program.cs
+
+
+ - Test 1: 插件系统在启动时初始化
+ - Test 2: Transform 被正确应用
+
+
+更新 Program.cs 集成插件系统:
+
+1. 注册插件服务:
+```csharp
+// 插件目录
+var pluginDirectory = configuration.GetValue("Plugin:Directory") ?? "plugins";
+
+// 插件主机
+builder.Services.AddSingleton(sp => new PluginHost(pluginDirectory));
+
+// Transform 提供者
+builder.Services.AddSingleton();
+
+// 目标选择器
+builder.Services.AddSingleton();
+
+// 配置监听器
+builder.Services.AddHostedService();
+```
+
+2. 初始化插件:
+```csharp
+var app = builder.Build();
+
+// 启动时加载插件
+var pluginHost = app.Services.GetRequiredService();
+await pluginHost.LoadAllAsync();
+```
+
+3. 配置 YARP 使用 Transform:
+```csharp
+builder.Services.AddReverseProxy()
+ .AddTransforms();
+```
+
+
+ dotnet build src/yarpgateway/YarpGateway.csproj
+
+
+ - Program.cs 正确集成插件系统
+ - 构建通过
+
+
+
+
+
+
+1. dotnet build src/yarpgateway/YarpGateway.csproj 无错误
+2. dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~Plugin" 通过
+3. 插件可通过 Metadata 启用
+4. Transform 正确应用到请求/响应
+
+
+
+- 插件通过 YARP Transform 管道处理请求
+- 目标选择在路由匹配后执行
+- 插件通过 Metadata 启用
+- Console DB 通知可触发插件重载
+- 所有单元测试通过
+
+
+
diff --git a/.planning/phases/006-gateway-plugin-research/006-RESEARCH.md b/.planning/phases/006-gateway-plugin-research/006-RESEARCH.md
new file mode 100644
index 0000000..15e7afd
--- /dev/null
+++ b/.planning/phases/006-gateway-plugin-research/006-RESEARCH.md
@@ -0,0 +1,284 @@
+# 阶段 6 研究:网关插件技术调研与实现
+
+**研究日期:** 2026-03-04
+**状态:** 已完成
+
+---
+
+## 1. 现有基础设施分析
+
+### 1.1 已有插件抽象层
+
+项目已创建 `Fengling.Gateway.Plugin.Abstractions` 程序集,定义了核心插件接口:
+
+```csharp
+// 已定义的接口
+public interface IGatewayPlugin
+{
+ string Name { get; }
+ string Version { get; }
+ string? Description { get; }
+ Task OnLoadAsync();
+ Task OnUnloadAsync();
+}
+
+public interface IRequestPlugin : IGatewayPlugin { ... }
+public interface IResponsePlugin : IGatewayPlugin { ... }
+public interface IRouteTransformPlugin : IGatewayPlugin { ... }
+public interface ILoadBalancePlugin : IGatewayPlugin { ... }
+```
+
+### 1.2 缺失的组件
+
+- ❌ **插件加载器** - 动态加载程序集的机制
+- ❌ **插件生命周期管理** - 加载/卸载/热重载
+- ❌ **插件隔离** - AssemblyLoadContext 隔离
+- ❌ **YARP 集成** - 将插件集成到 YARP 管道
+
+---
+
+## 2. YARP 扩展点
+
+### 2.1 中间件管道
+
+YARP 使用 ASP.NET Core 中间件管道,允许自定义注入点:
+
+```csharp
+app.MapReverseProxy(proxyPipeline =>
+{
+ proxyPipeline.Use(async (context, next) =>
+ {
+ // 插件前置执行
+ await pluginFeature.ExecutePreProxyAsync(context);
+ await next();
+ // 插件后置执行
+ await pluginFeature.ExecutePostProxyAsync(context);
+ });
+});
+```
+
+### 2.2 Transform 管道(推荐)
+
+Transform 是修改请求/响应的推荐方式:
+
+```csharp
+builder.Services.AddReverseProxy()
+ .AddTransforms(context =>
+ {
+ var plugins = PluginManager.GetTransformPlugins(context.Route);
+ foreach (var plugin in plugins)
+ {
+ context.AddRequestTransform(plugin.ApplyAsync);
+ }
+ });
+```
+
+### 2.3 路由扩展
+
+YARP 2.0+ 支持自定义路由元数据,插件可消费:
+
+```json
+{
+ "Routes": {
+ "api-route": {
+ "Extensions": {
+ "PluginConfig": {
+ "PluginId": "rate-limiter",
+ "MaxRequests": 100
+ }
+ }
+ }
+ }
+}
+```
+
+---
+
+## 3. .NET 插件加载最佳实践
+
+### 3.1 AssemblyLoadContext 隔离
+
+使用 **可卸载的 AssemblyLoadContext** 配合 **AssemblyDependencyResolver**:
+
+```csharp
+public sealed class PluginLoadContext : AssemblyLoadContext
+{
+ private readonly AssemblyDependencyResolver _resolver;
+ private readonly string _sharedAssemblyName = "Fengling.Gateway.Plugin.Abstractions";
+
+ public PluginLoadContext(string pluginPath) : base(isCollectible: true)
+ {
+ _resolver = new AssemblyDependencyResolver(pluginPath);
+ }
+
+ protected override Assembly? Load(AssemblyName assemblyName)
+ {
+ // 共享契约程序集(防止类型身份问题)
+ if (assemblyName.Name == _sharedAssemblyName)
+ return null; // 回退到默认 ALC
+
+ var path = _resolver.ResolveAssemblyToPath(assemblyName);
+ return path != null ? LoadFromAssemblyPath(path) : null;
+ }
+}
+```
+
+### 3.2 影子复制(热重载必需)
+
+Windows 锁定加载的 DLL,需要影子复制:
+
+```csharp
+public static string CreateShadowCopy(string pluginDirectory)
+{
+ var shadowDir = Path.Combine(
+ Path.GetTempPath(),
+ "PluginShadows",
+ $"{Path.GetFileName(pluginDirectory)}_{Guid.NewGuid():N}"
+ );
+ CopyDirectory(pluginDirectory, shadowDir);
+ return shadowDir;
+}
+```
+
+### 3.3 插件句柄模式
+
+```csharp
+public sealed class PluginHandle : IAsyncDisposable
+{
+ private readonly PluginLoadContext _alc;
+ private readonly IGatewayPlugin _plugin;
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_plugin is IAsyncDisposable ad) await ad.DisposeAsync();
+ _alc.Unload(); // 计划回收
+ }
+}
+```
+
+### 3.4 卸载验证
+
+```csharp
+public static bool VerifyUnload(WeakReference weakRef, int maxAttempts = 10)
+{
+ for (var i = 0; i < maxAttempts && weakRef.IsAlive; i++)
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ }
+ return !weakRef.IsAlive;
+}
+```
+
+---
+
+## 4. 插件隔离模式
+
+### 4.1 契约边界(最重要的设计决策)
+
+**规则**:契约必须驻留在 **默认 ALC**,绝不在插件 ALC 中。
+
+插件项目配置:
+```xml
+
+ false
+ runtime
+
+```
+
+### 4.2 静态缓存问题
+
+**问题**:主机单例缓存插件类型会阻止卸载。
+
+**解决方案**:只使用 DTO 跨越边界,不传递插件类型。
+
+---
+
+## 5. 实现架构
+
+### 5.1 组件结构
+
+```
+src/
+├── Fengling.Gateway.Plugin.Abstractions/ # 已存在
+│ └── IGatewayPlugin.cs
+├── yarpgateway/
+│ ├── Plugins/
+│ │ ├── PluginLoadContext.cs # ALC 隔离
+│ │ ├── PluginLoader.cs # 加载逻辑
+│ │ ├── PluginHost.cs # 生命周期管理
+│ │ └── PluginMiddleware.cs # YARP 集成
+│ └── Program.cs
+└── plugins/ # 插件目录
+ └── sample-plugin/
+ └── SamplePlugin.csproj
+```
+
+### 5.2 插件目录结构
+
+```
+plugins/
+├── rate-limiter/
+│ ├── RateLimiterPlugin.dll
+│ ├── RateLimiterPlugin.deps.json
+│ └── plugin.json # 元数据
+└── jwt-transform/
+ └── ...
+```
+
+### 5.3 插件元数据 (plugin.json)
+
+```json
+{
+ "id": "rate-limiter",
+ "name": "Rate Limiter Plugin",
+ "version": "1.0.0",
+ "entryPoint": "RateLimiterPlugin.RateLimiterPlugin",
+ "interfaces": ["IRequestPlugin"],
+ "dependencies": []
+}
+```
+
+---
+
+## 6. 验证架构
+
+### 6.1 测试策略
+
+1. **单元测试**:PluginLoadContext 隔离验证
+2. **集成测试**:插件加载/卸载/热重载
+3. **性能测试**:插件执行开销
+
+### 6.2 成功标准验证
+
+| 标准 | 验证方法 |
+|------|---------|
+| 动态加载插件 | 单元测试:从目录加载并执行 |
+| 插件相互隔离 | 单元测试:异常不传播到其他插件 |
+| 热加载/卸载 | 集成测试:WeakReference 验证卸载 |
+
+---
+
+## 7. 推荐库
+
+| 用途 | 库 |
+|------|---|
+| 网关核心 | Yarp.ReverseProxy 2.3.0+ |
+| 插件加载 | 自定义 AssemblyLoadContext |
+| 元数据读取 | System.Reflection.Metadata |
+| 依赖注入 | Microsoft.Extensions.DependencyInjection |
+
+---
+
+## 8. 关键注意事项
+
+1. **不要在主机单例中缓存插件类型**
+2. **始终用 WeakReference 测试卸载**
+3. **Windows 上必须影子复制以支持热重载**
+4. **使用仅元数据发现避免仅扫描而加载程序集**
+5. **谨慎处理原生依赖**(它们不能干净卸载)
+
+---
+
+*研究完成:2026-03-04*
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
index 2358532..ff592fa 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -11,6 +11,8 @@
Npgsql.*
StackExchange.Redis
Yarp.*
+ YamlDotNet
+ System.CommandLine
xunit
Moq
FluentAssertions
diff --git a/MigrationTask.sln b/MigrationTask.sln
new file mode 100644
index 0000000..17e1130
--- /dev/null
+++ b/MigrationTask.sln
@@ -0,0 +1,21 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.13.35828.75
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MigrationTool", "tools\MigrationTool\MigrationTool.csproj", "{A1B2C3D4-5E6F-7A8B-9C0D-1E2F3A4B5C6D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A1B2C3D4-5E6F-7A8B-9C0D-1E2F3A4B5C6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1B2C3D4-5E6F-7A8B-9C0D-1E2F3A4B5C6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1B2C3D4-5E6F-7A8B-9C0D-1E2F3A4B5C6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1B2C3D4-5E6F-7A8B-9C0D-1E2F3A4B5C6D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/src/yarpgateway/Plugins/PluginHost.cs b/src/yarpgateway/Plugins/PluginHost.cs
new file mode 100644
index 0000000..741fa4f
--- /dev/null
+++ b/src/yarpgateway/Plugins/PluginHost.cs
@@ -0,0 +1,208 @@
+using System.Collections.Concurrent;
+using Fengling.Gateway.Plugin.Abstractions;
+
+namespace YarpGateway.Plugins;
+
+///
+/// 插件句柄 - 封装插件实例和加载上下文
+///
+public sealed class PluginHandle : IAsyncDisposable
+{
+ private readonly PluginLoadContext _alc;
+ private readonly string _shadowDirectory;
+ private bool _disposed;
+
+ public IGatewayPlugin Plugin { get; }
+ public string PluginId { get; }
+ public WeakReference TrackUnloadability() => new(_alc, trackResurrection: true);
+
+ public PluginHandle(string pluginId, IGatewayPlugin plugin, PluginLoadContext alc, string shadowDirectory)
+ {
+ PluginId = pluginId;
+ Plugin = plugin;
+ _alc = alc;
+ _shadowDirectory = shadowDirectory;
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ // 调用插件卸载回调
+ await Plugin.OnUnloadAsync();
+
+ // 卸载 ALC
+ _alc.Unload();
+
+ // 删除影子目录
+ try
+ {
+ if (Directory.Exists(_shadowDirectory))
+ {
+ Directory.Delete(_shadowDirectory, recursive: true);
+ }
+ }
+ catch
+ {
+ // 忽略清理错误
+ }
+ }
+}
+
+///
+/// 插件主机 - 管理所有已加载的插件
+///
+public class PluginHost
+{
+ private readonly ConcurrentDictionary _plugins = new();
+ private readonly PluginLoader _loader;
+ private readonly string _pluginDirectory;
+ private readonly string _shadowDirectory;
+
+ public PluginHost(string pluginDirectory)
+ {
+ _pluginDirectory = pluginDirectory;
+ _loader = new PluginLoader();
+ _shadowDirectory = Path.Combine(Path.GetTempPath(), "PluginShadows", Guid.NewGuid().ToString());
+ }
+
+ ///
+ /// 获取当前加载的所有插件
+ ///
+ public IEnumerable GetPlugins()
+ {
+ return _plugins.Values.Select(h => h.Plugin);
+ }
+
+ ///
+ /// 获取插件信息
+ ///
+ public IEnumerable<(string Id, IGatewayPlugin Plugin)> GetPluginInfo()
+ {
+ return _plugins.Values.Select(h => (h.PluginId, h.Plugin));
+ }
+
+ ///
+ /// 加载目录中的所有插件
+ ///
+ public async Task LoadAllAsync(CancellationToken ct = default)
+ {
+ if (!Directory.Exists(_pluginDirectory))
+ {
+ return 0;
+ }
+
+ var discoveredPlugins = _loader.DiscoverPlugins(_pluginDirectory).ToList();
+ var loadedCount = 0;
+
+ foreach (var discovered in discoveredPlugins)
+ {
+ if (ct.IsCancellationRequested) break;
+
+ var handle = await LoadSinglePluginAsync(discovered);
+ if (handle != null)
+ {
+ _plugins[discovered.Id] = handle;
+ loadedCount++;
+ }
+ }
+
+ return loadedCount;
+ }
+
+ ///
+ ///
+ /// 加载单个插件
+ ///
+ private async Task LoadSinglePluginAsync(DiscoveredPlugin discovered)
+ {
+ try
+ {
+ // 创建影子副本
+ var shadowDir = Path.Combine(_shadowDirectory, discovered.Id);
+ var shadowPath = PluginLoader.CreateShadowCopy(discovered.AssemblyPath, shadowDir);
+
+ // 创建 ALC
+ var alc = new PluginLoadContext(shadowDir);
+
+ // 加载插件
+ var plugin = PluginLoader.LoadPlugin(discovered, alc);
+ if (plugin == null)
+ {
+ return null;
+ }
+
+ // 调用加载回调
+ await plugin.OnLoadAsync();
+
+ return new PluginHandle(discovered.Id, plugin, alc, shadowDir);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// 卸载指定插件
+ ///
+ public async Task UnloadAsync(string pluginId)
+ {
+ if (_plugins.TryRemove(pluginId, out var handle))
+ {
+ await handle.DisposeAsync();
+ }
+ }
+
+ ///
+ /// 重新加载指定插件
+ ///
+ public async Task ReloadAsync(string pluginId)
+ {
+ // 查找已发现的插件信息
+ var discovered = _loader.DiscoverPlugins(_pluginDirectory)
+ .FirstOrDefault(p => p.Id == pluginId);
+
+ if (discovered == null)
+ {
+ return;
+ }
+
+ // 卸载旧插件
+ await UnloadAsync(pluginId);
+
+ // 加载新插件
+ var handle = await LoadSinglePluginAsync(discovered);
+ if (handle != null)
+ {
+ _plugins[pluginId] = handle;
+ }
+ }
+
+ ///
+ /// 卸载所有插件
+ ///
+ public async Task UnloadAllAsync()
+ {
+ var pluginIds = _plugins.Keys.ToList();
+ foreach (var id in pluginIds)
+ {
+ await UnloadAsync(id);
+ }
+ }
+
+ ///
+ /// 验证插件是否已卸载
+ ///
+ public static bool VerifyUnload(WeakReference weakRef, int maxAttempts = 10)
+ {
+ for (var i = 0; i < maxAttempts && weakRef.IsAlive; i++)
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ }
+ return !weakRef.IsAlive;
+ }
+}
diff --git a/src/yarpgateway/Plugins/PluginLoadContext.cs b/src/yarpgateway/Plugins/PluginLoadContext.cs
new file mode 100644
index 0000000..9ba1ca3
--- /dev/null
+++ b/src/yarpgateway/Plugins/PluginLoadContext.cs
@@ -0,0 +1,82 @@
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace YarpGateway.Plugins;
+
+///
+/// 可卸载的 AssemblyLoadContext,用于插件隔离
+///
+public sealed class PluginLoadContext : AssemblyLoadContext
+{
+ private readonly AssemblyDependencyResolver? _resolver;
+ private readonly string _sharedAssemblyName = "Fengling.Gateway.Plugin.Abstractions";
+
+ ///
+ /// 创建插件加载上下文
+ ///
+ /// 插件目录路径
+ public PluginLoadContext(string pluginPath) : base(isCollectible: true)
+ {
+ // AssemblyDependencyResolver 需要有效的插件目录,否则会抛出异常
+ if (Directory.Exists(pluginPath) && File.Exists(Path.Combine(pluginPath, Path.GetFileName(pluginPath) + ".deps.json")))
+ {
+ _resolver = new AssemblyDependencyResolver(pluginPath);
+ }
+ }
+
+ ///
+ /// 加载程序集
+ ///
+ public Assembly? LoadAssembly(AssemblyName assemblyName)
+ {
+ // 共享契约程序集使用默认 ALC,避免类型身份问题
+ if (assemblyName.Name == _sharedAssemblyName)
+ {
+ return null;
+ }
+
+ // 尝试使用 resolver 解析
+ if (_resolver != null)
+ {
+ var path = _resolver.ResolveAssemblyToPath(assemblyName);
+ if (path != null)
+ {
+ return LoadFromAssemblyPath(path);
+ }
+ }
+
+ // 尝试从当前目录加载
+ var assemblyDir = AppContext.BaseDirectory;
+ var assemblyPath = Path.Combine(assemblyDir, assemblyName.Name + ".dll");
+ if (File.Exists(assemblyPath))
+ {
+ return LoadFromAssemblyPath(assemblyPath);
+ }
+
+ return null;
+ }
+
+ ///
+ /// 加载程序集(内部调用)
+ ///
+ protected override Assembly? Load(AssemblyName assemblyName)
+ {
+ return LoadAssembly(assemblyName);
+ }
+
+ ///
+ /// 加载非托管 DLL
+ ///
+ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
+ {
+ if (_resolver != null)
+ {
+ var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
+ if (path != null)
+ {
+ return LoadUnmanagedDllFromPath(path);
+ }
+ }
+ return IntPtr.Zero;
+ }
+}
diff --git a/src/yarpgateway/Plugins/PluginLoader.cs b/src/yarpgateway/Plugins/PluginLoader.cs
new file mode 100644
index 0000000..a6d192a
--- /dev/null
+++ b/src/yarpgateway/Plugins/PluginLoader.cs
@@ -0,0 +1,157 @@
+using System.Reflection;
+using System.Text.Json;
+using Fengling.Gateway.Plugin.Abstractions;
+using Fengling.Gateway.Plugin.Abstractions;
+
+namespace YarpGateway.Plugins;
+
+///
+/// 发现的插件信息
+///
+public class DiscoveredPlugin
+{
+ public required string Id { get; init; }
+ public required string Name { get; init; }
+ public required string Version { get; init; }
+ public required string AssemblyPath { get; init; }
+ public required string EntryPoint { get; init; }
+ public string? Description { get; init; }
+}
+
+///
+/// 插件元数据
+///
+public class PluginMetadata
+{
+ public string Id { get; set; } = string.Empty;
+ public string Name { get; set; } = string.Empty;
+ public string Version { get; set; } = "1.0.0";
+ public string EntryPoint { get; set; } = string.Empty;
+ public string? Description { get; set; }
+}
+
+///
+/// 插件加载器 - 发现和加载插件
+///
+public class PluginLoader
+{
+ private const string PluginManifestFileName = "plugin.json";
+
+ ///
+ /// 从目录发现所有插件
+ ///
+ public IEnumerable DiscoverPlugins(string pluginDirectory)
+ {
+ if (!Directory.Exists(pluginDirectory))
+ {
+ yield break;
+ }
+
+ foreach (var pluginDir in Directory.GetDirectories(pluginDirectory))
+ {
+ var metadata = LoadPluginMetadata(pluginDir);
+ if (metadata == null)
+ {
+ continue;
+ }
+
+ var dllPath = Path.Combine(pluginDir, metadata.Id + ".dll");
+ if (!File.Exists(dllPath))
+ {
+ continue;
+ }
+
+ yield return new DiscoveredPlugin
+ {
+ Id = metadata.Id,
+ Name = metadata.Name,
+ Version = metadata.Version,
+ Description = metadata.Description,
+ AssemblyPath = dllPath,
+ EntryPoint = metadata.EntryPoint
+ };
+ }
+ }
+
+ ///
+ /// 加载插件元数据
+ ///
+ private static PluginMetadata? LoadPluginMetadata(string pluginDir)
+ {
+ var manifestPath = Path.Combine(pluginDir, PluginManifestFileName);
+ if (!File.Exists(manifestPath))
+ {
+ return null;
+ }
+
+ try
+ {
+ var json = File.ReadAllText(manifestPath);
+ return JsonSerializer.Deserialize(json, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// 创建插件的影子副本(用于热重载)
+ ///
+ public static string CreateShadowCopy(string sourcePath, string shadowDirectory)
+ {
+ Directory.CreateDirectory(shadowDirectory);
+
+ var fileName = Path.GetFileName(sourcePath);
+ var destPath = Path.Combine(shadowDirectory, fileName);
+
+ // 只复制 DLL 文件(如果需要,可以扩展到其他文件)
+ if (File.Exists(sourcePath))
+ {
+ File.Copy(sourcePath, destPath, overwrite: true);
+ }
+
+ // 复制 deps.json
+ var depsPath = sourcePath + ".deps.json";
+ if (File.Exists(depsPath))
+ {
+ File.Copy(depsPath, Path.Combine(shadowDirectory, fileName + ".deps.json"), overwrite: true);
+ }
+
+ // 复制 runtimeconfig.json
+ var runtimeConfigPath = sourcePath + ".runtimeconfig.json";
+ if (File.Exists(runtimeConfigPath))
+ {
+ File.Copy(runtimeConfigPath, Path.Combine(shadowDirectory, fileName + ".runtimeconfig.json"), overwrite: true);
+ }
+
+ return destPath;
+ }
+
+ ///
+ /// 加载插件程序集并创建实例
+ ///
+ public static IGatewayPlugin? LoadPlugin(DiscoveredPlugin discovered, PluginLoadContext alc)
+ {
+ try
+ {
+ var assemblyName = Path.GetFileNameWithoutExtension(discovered.AssemblyPath);
+ var assembly = alc.LoadFromAssemblyName(new AssemblyName(assemblyName));
+
+ var type = assembly.GetType(discovered.EntryPoint);
+ if (type == null)
+ {
+ return null;
+ }
+
+ return Activator.CreateInstance(type) as IGatewayPlugin;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/tests/YarpGateway.Tests/Unit/Plugins/PluginHostTests.cs b/tests/YarpGateway.Tests/Unit/Plugins/PluginHostTests.cs
new file mode 100644
index 0000000..ace3960
--- /dev/null
+++ b/tests/YarpGateway.Tests/Unit/Plugins/PluginHostTests.cs
@@ -0,0 +1,138 @@
+using Fengling.Gateway.Plugin.Abstractions;
+using Xunit;
+using YarpGateway.Plugins;
+
+namespace YarpGateway.Tests.Unit.Plugins;
+
+public class PluginHostTests : IDisposable
+{
+ private readonly string _testDir;
+
+ public PluginHostTests()
+ {
+ _testDir = Path.Combine(Path.GetTempPath(), "plugin-host-test-" + Guid.NewGuid());
+ Directory.CreateDirectory(_testDir);
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ Directory.Delete(_testDir, true);
+ }
+ catch { }
+ }
+
+ [Fact]
+ public void Constructor_ShouldInitialize()
+ {
+ // Act
+ var host = new PluginHost(_testDir);
+
+ // Assert
+ Assert.NotNull(host);
+ }
+
+ [Fact]
+ public void GetPlugins_Empty_ShouldReturnEmpty()
+ {
+ // Arrange
+ var host = new PluginHost(_testDir);
+
+ // Act
+ var plugins = host.GetPlugins().ToList();
+
+ // Assert
+ Assert.Empty(plugins);
+ }
+
+ [Fact]
+ public void LoadAllAsync_EmptyDirectory_ShouldReturnZero()
+ {
+ // Arrange
+ var host = new PluginHost(_testDir);
+
+ // Act
+ var count = host.LoadAllAsync().Result;
+
+ // Assert
+ Assert.Equal(0, count);
+ }
+
+ [Fact]
+ public async Task GetPluginInfo_AfterLoad_ShouldReturnPlugins()
+ {
+ // Arrange
+ var host = new PluginHost(_testDir);
+
+ // 创建测试插件目录
+ var pluginDir = Path.Combine(_testDir, "test-plugin");
+ Directory.CreateDirectory(pluginDir);
+
+ // 创建 plugin.json
+ var manifest = new
+ {
+ id = "test-plugin",
+ name = "Test Plugin",
+ version = "1.0.0",
+ entryPoint = "TestPlugin.TestPlugin"
+ };
+ File.WriteAllText(Path.Combine(pluginDir, "plugin.json"), System.Text.Json.JsonSerializer.Serialize(manifest));
+
+ // 创建 DLL(占位符,不会真正加载)
+ File.WriteAllText(Path.Combine(pluginDir, "test-plugin.dll"), "dummy");
+
+ // Act
+ var count = await host.LoadAllAsync();
+
+ // Assert - 加载会失败因为 DLL 是无效的,但应该返回 0
+ Assert.Equal(0, count);
+ }
+
+ [Fact]
+ public async Task UnloadAsync_NotLoaded_ShouldNotThrow()
+ {
+ // Arrange
+ var host = new PluginHost(_testDir);
+
+ // Act & Assert - 不应抛出异常
+ await host.UnloadAsync("non-existent");
+ }
+
+ [Fact]
+ public async Task UnloadAllAsync_Empty_ShouldNotThrow()
+ {
+ // Arrange
+ var host = new PluginHost(_testDir);
+
+ // Act & Assert - 不应抛出异常
+ await host.UnloadAllAsync();
+ }
+
+ [Fact]
+ public void VerifyUnload_AliveObject_ShouldReturnFalse()
+ {
+ // Arrange
+ var obj = new object();
+ var weakRef = new WeakReference(obj);
+
+ // Act
+ var result = PluginHost.VerifyUnload(weakRef, maxAttempts: 1);
+
+ // Assert
+ Assert.False(result);
+ }
+}
+
+///
+/// 测试用插件实现
+///
+public class TestPlugin : IGatewayPlugin
+{
+ public string Name => "TestPlugin";
+ public string Version => "1.0.0";
+ public string? Description => "A test plugin";
+
+ public Task OnLoadAsync() => Task.CompletedTask;
+ public Task OnUnloadAsync() => Task.CompletedTask;
+}
diff --git a/tests/YarpGateway.Tests/Unit/Plugins/PluginLoadContextTests.cs b/tests/YarpGateway.Tests/Unit/Plugins/PluginLoadContextTests.cs
new file mode 100644
index 0000000..068625b
--- /dev/null
+++ b/tests/YarpGateway.Tests/Unit/Plugins/PluginLoadContextTests.cs
@@ -0,0 +1,64 @@
+using System.Reflection;
+using Xunit;
+using YarpGateway.Plugins;
+
+namespace YarpGateway.Tests.Unit.Plugins;
+
+public class PluginLoadContextTests
+{
+ private const string SharedAssemblyName = "Fengling.Gateway.Plugin.Abstractions";
+
+ [Fact]
+ public void Constructor_ShouldInitializeWithPluginPath()
+ {
+ // Arrange
+ var pluginPath = Path.Combine(Path.GetTempPath(), "test-plugin");
+
+ // Act
+ var context = new PluginLoadContext(pluginPath);
+
+ // Assert
+ Assert.NotNull(context);
+ }
+
+ [Fact]
+ public void LoadAssembly_SharedAssembly_ShouldReturnNull_UseDefaultALC()
+ {
+ // Arrange
+ var pluginPath = Path.Combine(Path.GetTempPath(), "test-plugin");
+ var context = new PluginLoadContext(pluginPath);
+ var assemblyName = new AssemblyName(SharedAssemblyName);
+
+ // Act
+ var assembly = context.LoadAssembly(assemblyName);
+
+ // Assert - null means use default ALC
+ Assert.Null(assembly);
+ }
+
+ [Fact]
+ public void LoadAssembly_UnknownAssembly_ShouldReturnNull()
+ {
+ // Arrange
+ var pluginPath = Path.Combine(Path.GetTempPath(), "test-plugin");
+ var context = new PluginLoadContext(pluginPath);
+ var assemblyName = new AssemblyName("NonExistentAssembly");
+
+ // Act
+ var assembly = context.LoadAssembly(assemblyName);
+
+ // Assert
+ Assert.Null(assembly);
+ }
+
+ [Fact]
+ public void IsCollectible_ShouldBeTrue()
+ {
+ // Arrange
+ var pluginPath = Path.Combine(Path.GetTempPath(), "test-plugin");
+ var context = new PluginLoadContext(pluginPath);
+
+ // Assert
+ Assert.True(context.IsCollectible);
+ }
+}
diff --git a/tests/YarpGateway.Tests/Unit/Plugins/PluginLoaderTests.cs b/tests/YarpGateway.Tests/Unit/Plugins/PluginLoaderTests.cs
new file mode 100644
index 0000000..4c7aa5b
--- /dev/null
+++ b/tests/YarpGateway.Tests/Unit/Plugins/PluginLoaderTests.cs
@@ -0,0 +1,134 @@
+using Fengling.Gateway.Plugin.Abstractions;
+using Xunit;
+using YarpGateway.Plugins;
+
+namespace YarpGateway.Tests.Unit.Plugins;
+
+public class PluginLoaderTests
+{
+ private string _testBaseDir = null!;
+
+ public PluginLoaderTests()
+ {
+ _testBaseDir = Path.Combine(Path.GetTempPath(), "plugin-test-" + Guid.NewGuid());
+ }
+
+ [Fact]
+ public void DiscoverPlugins_EmptyDirectory_ShouldReturnEmpty()
+ {
+ // Arrange
+ var loader = new PluginLoader();
+ var emptyDir = Path.Combine(_testBaseDir, "empty");
+ Directory.CreateDirectory(emptyDir);
+
+ try
+ {
+ // Act
+ var plugins = loader.DiscoverPlugins(emptyDir).ToList();
+
+ // Assert
+ Assert.Empty(plugins);
+ }
+ finally
+ {
+ Directory.Delete(_testBaseDir, true);
+ }
+ }
+
+ [Fact]
+ public void DiscoverPlugins_ValidPlugin_ShouldReturnPlugin()
+ {
+ // Arrange
+ var loader = new PluginLoader();
+ var pluginDir = Path.Combine(_testBaseDir, "test-plugin");
+ Directory.CreateDirectory(pluginDir);
+
+ // 创建 plugin.json
+ var manifest = new
+ {
+ id = "test-plugin",
+ name = "Test Plugin",
+ version = "1.0.0",
+ entryPoint = "TestPlugin.TestPlugin",
+ description = "A test plugin"
+ };
+ var manifestJson = System.Text.Json.JsonSerializer.Serialize(manifest);
+ File.WriteAllText(Path.Combine(pluginDir, "plugin.json"), manifestJson);
+
+ // 创建一个占位 DLL
+ File.WriteAllText(Path.Combine(pluginDir, "test-plugin.dll"), "dummy");
+
+ try
+ {
+ // Act
+ var plugins = loader.DiscoverPlugins(_testBaseDir).ToList();
+
+ // Assert
+ Assert.Single(plugins);
+ var plugin = plugins[0];
+ Assert.Equal("test-plugin", plugin.Id);
+ Assert.Equal("Test Plugin", plugin.Name);
+ Assert.Equal("1.0.0", plugin.Version);
+ }
+ finally
+ {
+ Directory.Delete(_testBaseDir, true);
+ }
+ }
+
+ [Fact]
+ public void DiscoverPlugins_NoManifest_ShouldSkipPlugin()
+ {
+ // Arrange
+ var loader = new PluginLoader();
+ var pluginDir = Path.Combine(_testBaseDir, "test-plugin");
+ Directory.CreateDirectory(pluginDir);
+
+ // 只创建 DLL,没有 plugin.json
+ File.WriteAllText(Path.Combine(pluginDir, "test-plugin.dll"), "dummy");
+
+ try
+ {
+ // Act
+ var plugins = loader.DiscoverPlugins(_testBaseDir).ToList();
+
+ // Assert
+ Assert.Empty(plugins);
+ }
+ finally
+ {
+ Directory.Delete(_testBaseDir, true);
+ }
+ }
+
+ [Fact]
+ public void CreateShadowCopy_ShouldCopyFiles()
+ {
+ // Arrange
+ var sourceDir = Path.Combine(_testBaseDir, "source");
+ var shadowDir = Path.Combine(_testBaseDir, "shadow");
+ Directory.CreateDirectory(sourceDir);
+
+ // 创建源 DLL
+ var dllPath = Path.Combine(sourceDir, "TestPlugin.dll");
+ File.WriteAllText(dllPath, "dummy dll");
+
+ // 创建 deps.json
+ var depsPath = dllPath + ".deps.json";
+ File.WriteAllText(depsPath, "{}");
+
+ try
+ {
+ // Act
+ var shadowPath = PluginLoader.CreateShadowCopy(dllPath, shadowDir);
+
+ // Assert
+ Assert.True(File.Exists(shadowPath));
+ Assert.True(File.Exists(Path.Combine(shadowDir, "TestPlugin.dll.deps.json")));
+ }
+ finally
+ {
+ Directory.Delete(_testBaseDir, true);
+ }
+ }
+}
diff --git a/tools/MigrationTool/MigrationOptions.cs b/tools/MigrationTool/MigrationOptions.cs
new file mode 100644
index 0000000..d25f01a
--- /dev/null
+++ b/tools/MigrationTool/MigrationOptions.cs
@@ -0,0 +1,62 @@
+namespace MigrationTool;
+
+///
+/// 迁移工具命令行选项
+///
+public class MigrationOptions
+{
+ ///
+ /// 数据库连接字符串
+ ///
+ public string ConnectionString { get; set; } = string.Empty;
+
+ ///
+ /// 输出目录
+ ///
+ public string OutputDir { get; set; } = "./output";
+
+ ///
+ /// 是否 Dry-Run 模式(只输出不写入文件)
+ ///
+ public bool DryRun { get; set; }
+
+ ///
+ /// 默认路由 Host
+ ///
+ public string DefaultHost { get; set; } = "api.fengling.com";
+
+ ///
+ /// 服务端口
+ ///
+ public int ServicePort { get; set; } = 80;
+
+ ///
+ /// 目标端口
+ ///
+ public int TargetPort { get; set; } = 8080;
+
+ ///
+ /// 是否验证数据完整性
+ ///
+ public bool Validate { get; set; } = true;
+
+ ///
+ /// 仅处理指定租户
+ ///
+ public string? TenantCode { get; set; }
+
+ ///
+ /// 日志级别
+ ///
+ public LogLevel LogLevel { get; set; } = LogLevel.Information;
+
+ ///
+ /// 是否生成报告文件
+ ///
+ public bool GenerateReport { get; set; } = true;
+
+ ///
+ /// 报告文件路径
+ ///
+ public string? ReportPath { get; set; }
+}
diff --git a/tools/MigrationTool/MigrationService.cs b/tools/MigrationTool/MigrationService.cs
new file mode 100644
index 0000000..9fcbef9
--- /dev/null
+++ b/tools/MigrationTool/MigrationService.cs
@@ -0,0 +1,521 @@
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using MigrationTool.Models;
+using Npgsql;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
+
+namespace MigrationTool;
+
+///
+/// 迁移服务 - 处理从数据库读取配置并生成 K8s Service YAML
+///
+public class MigrationService
+{
+ private readonly MigrationOptions _options;
+ private readonly ILogger _logger;
+ private readonly ISerializer _yamlSerializer;
+
+ public MigrationService(MigrationOptions options, ILogger logger)
+ {
+ _options = options;
+ _logger = logger;
+ _yamlSerializer = new SerializerBuilder()
+ .WithNamingConvention(CamelCaseNamingConvention.Instance)
+ .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull)
+ .Build();
+ }
+
+ ///
+ /// 执行迁移
+ ///
+ public async Task MigrateAsync(CancellationToken cancellationToken = default)
+ {
+ var report = new MigrationReport
+ {
+ StartTime = DateTime.UtcNow,
+ Entries = []
+ };
+
+ _logger.LogInformation("开始迁移任务...");
+ _logger.LogInformation($"连接字符串: {MaskConnectionString(_options.ConnectionString)}");
+ _logger.LogInformation($"输出目录: {Path.GetFullPath(_options.OutputDir)}");
+ _logger.LogInformation($"Dry-Run 模式: {_options.DryRun}");
+ _logger.LogInformation($"验证模式: {_options.Validate}");
+
+ // 确保输出目录存在(如果不是 dry-run)
+ if (!_options.DryRun)
+ {
+ Directory.CreateDirectory(_options.OutputDir);
+ _logger.LogInformation($"已创建输出目录: {Path.GetFullPath(_options.OutputDir)}");
+ }
+
+ try
+ {
+ // 从数据库读取配置
+ var routes = await LoadRoutesAsync(cancellationToken);
+ var clusters = await LoadClustersAsync(cancellationToken);
+
+ _logger.LogInformation($"从数据库加载了 {routes.Count} 条路由和 {clusters.Count} 个集群");
+
+ report.TotalRoutes = routes.Count;
+
+ // 验证数据完整性
+ if (_options.Validate)
+ {
+ var validationResults = ValidateData(routes, clusters);
+ if (validationResults.Any(r => !r.IsValid))
+ {
+ _logger.LogWarning($"数据验证发现 {validationResults.Count(r => !r.IsValid)} 个问题");
+ foreach (var result in validationResults.Where(r => !r.IsValid))
+ {
+ _logger.LogWarning($"验证失败: {result.Message}");
+ }
+ }
+ else
+ {
+ _logger.LogInformation("数据验证通过");
+ }
+ }
+
+ // 处理每条路由
+ foreach (var route in routes)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ _logger.LogWarning("迁移任务已取消");
+ break;
+ }
+
+ var entry = await ProcessRouteAsync(route, clusters, cancellationToken);
+ report.Entries.Add(entry);
+
+ switch (entry.Status)
+ {
+ case MigrationStatus.Success:
+ report.SuccessCount++;
+ break;
+ case MigrationStatus.Failed:
+ report.FailedCount++;
+ break;
+ case MigrationStatus.Skipped:
+ report.SkippedCount++;
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"迁移过程中发生错误: {ex.Message}");
+ throw;
+ }
+ finally
+ {
+ report.EndTime = DateTime.UtcNow;
+ }
+
+ // 生成报告
+ if (_options.GenerateReport)
+ {
+ await SaveReportAsync(report, cancellationToken);
+ }
+
+ return report;
+ }
+
+ ///
+ /// 从数据库加载路由配置
+ ///
+ private async Task> LoadRoutesAsync(CancellationToken cancellationToken)
+ {
+ var routes = new List();
+
+ await using var connection = new NpgsqlConnection(_options.ConnectionString);
+ await connection.OpenAsync(cancellationToken);
+
+ var sql = @"
+ SELECT id, tenant_code, service_name, cluster_id, path_pattern,
+ match, priority, status, is_global, is_deleted
+ FROM tenant_routes
+ WHERE is_deleted = false AND status = 1";
+
+ if (!string.IsNullOrEmpty(_options.TenantCode))
+ {
+ sql += " AND tenant_code = @tenantCode";
+ }
+
+ sql += " ORDER BY tenant_code, service_name";
+
+ await using var command = new NpgsqlCommand(sql, connection);
+ if (!string.IsNullOrEmpty(_options.TenantCode))
+ {
+ command.Parameters.AddWithValue("@tenantCode", _options.TenantCode);
+ }
+
+ await using var reader = await command.ExecuteReaderAsync(cancellationToken);
+ while (await reader.ReadAsync(cancellationToken))
+ {
+ var route = new GwTenantRouteModel
+ {
+ Id = reader.GetInt64(0),
+ TenantCode = reader.GetString(1),
+ ServiceName = reader.GetString(2),
+ ClusterId = reader.GetString(3),
+ PathPattern = reader.GetString(4),
+ MatchJson = reader.IsDBNull(5) ? null : reader.GetString(5),
+ Priority = reader.GetInt32(6),
+ Status = reader.GetInt32(7),
+ IsGlobal = reader.GetBoolean(8),
+ IsDeleted = reader.GetBoolean(9)
+ };
+ routes.Add(route);
+ }
+
+ return routes;
+ }
+
+ ///
+ /// 从数据库加载集群配置
+ ///
+ private async Task> LoadClustersAsync(CancellationToken cancellationToken)
+ {
+ var clusters = new List();
+
+ await using var connection = new NpgsqlConnection(_options.ConnectionString);
+ await connection.OpenAsync(cancellationToken);
+
+ const string sql = @"
+ SELECT id, cluster_id, name, description, destinations, status, is_deleted
+ FROM clusters
+ WHERE is_deleted = false AND status = 1";
+
+ await using var command = new NpgsqlCommand(sql, connection);
+ await using var reader = await command.ExecuteReaderAsync(cancellationToken);
+ while (await reader.ReadAsync(cancellationToken))
+ {
+ var cluster = new GwClusterModel
+ {
+ Id = reader.GetString(0),
+ ClusterId = reader.GetString(1),
+ Name = reader.GetString(2),
+ Description = reader.IsDBNull(3) ? null : reader.GetString(3),
+ DestinationsJson = reader.IsDBNull(4) ? null : reader.GetString(4),
+ Status = reader.GetInt32(5),
+ IsDeleted = reader.GetBoolean(6)
+ };
+ clusters.Add(cluster);
+ }
+
+ return clusters;
+ }
+
+ ///
+ /// 处理单条路由
+ ///
+ private async Task ProcessRouteAsync(
+ GwTenantRouteModel route,
+ List clusters,
+ CancellationToken cancellationToken)
+ {
+ var entry = new MigrationEntry
+ {
+ ServiceName = route.ServiceName,
+ TenantCode = route.TenantCode,
+ ClusterId = route.ClusterId,
+ Status = MigrationStatus.Success
+ };
+
+ _logger.LogDebug($"处理路由: {route.ServiceName} (租户: {route.TenantCode})");
+
+ try
+ {
+ // 查找对应的集群
+ var cluster = clusters.FirstOrDefault(c => c.ClusterId == route.ClusterId);
+ if (cluster == null)
+ {
+ _logger.LogWarning($"路由 {route.ServiceName} 引用的集群 {route.ClusterId} 不存在,跳过");
+ entry.Status = MigrationStatus.Skipped;
+ entry.ErrorMessage = $"集群 {route.ClusterId} 不存在";
+ return entry;
+ }
+
+ // 获取目标端点
+ var destinations = cluster.GetDestinations();
+ var destination = destinations.FirstOrDefault(d => d.Status == 1)
+ ?? destinations.FirstOrDefault();
+
+ if (destination == null)
+ {
+ _logger.LogWarning($"集群 {cluster.ClusterId} 没有可用的目标端点");
+ }
+
+ // 生成 Service YAML
+ var serviceYaml = GenerateServiceYaml(route, cluster, destination);
+
+ // 确定输出文件名
+ var fileName = $"{route.TenantCode}-{route.ServiceName}.yaml".ToLowerInvariant();
+ var outputPath = Path.Combine(_options.OutputDir, fileName);
+ entry.OutputPath = outputPath;
+
+ if (_options.DryRun)
+ {
+ _logger.LogInformation($"[DRY-RUN] 将生成: {fileName}");
+ _logger.LogDebug($"YAML 内容:\n{serviceYaml}");
+ }
+ else
+ {
+ await File.WriteAllTextAsync(outputPath, serviceYaml, cancellationToken);
+ _logger.LogInformation($"已生成: {outputPath}");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"处理路由 {route.ServiceName} 时出错: {ex.Message}");
+ entry.Status = MigrationStatus.Failed;
+ entry.ErrorMessage = ex.Message;
+ }
+
+ return entry;
+ }
+
+ ///
+ /// 生成 K8s Service YAML
+ ///
+ private string GenerateServiceYaml(
+ GwTenantRouteModel route,
+ GwClusterModel cluster,
+ GwDestinationModel? destination)
+ {
+ var serviceName = $"{route.TenantCode}-{route.ServiceName}".ToLowerInvariant();
+ var path = route.GetPath();
+ var host = route.GetHost() ?? _options.DefaultHost;
+ var destinationId = destination?.DestinationId ?? "default";
+
+ var model = new K8sServiceModel
+ {
+ Metadata = new K8sMetadata
+ {
+ Name = serviceName,
+ Labels = new Dictionary
+ {
+ ["app-router-host"] = host,
+ ["app-router-name"] = route.ServiceName,
+ ["app-router-prefix"] = path,
+ ["app-cluster-name"] = cluster.ClusterId,
+ ["app-cluster-destination"] = destinationId,
+ ["app-tenant"] = route.TenantCode,
+ ["app-managed-by"] = "migration-tool"
+ },
+ Annotations = new Dictionary
+ {
+ ["migration-tool/fengling.gateway/route-id"] = route.Id.ToString(),
+ ["migration-tool/fengling.gateway/cluster-id"] = cluster.Id,
+ ["migration-tool/fengling.gateway/priority"] = route.Priority.ToString(),
+ ["migration-tool/timestamp"] = DateTime.UtcNow.ToString("O")
+ }
+ },
+ Spec = new K8sSpec
+ {
+ Type = "ClusterIP",
+ Selector = new Dictionary
+ {
+ ["app"] = route.ServiceName.ToLowerInvariant()
+ },
+ Ports =
+ [
+ new K8sPort
+ {
+ Port = _options.ServicePort,
+ TargetPort = _options.TargetPort,
+ Name = "http",
+ Protocol = "TCP"
+ }
+ ]
+ }
+ };
+
+ var yaml = _yamlSerializer.Serialize(model);
+
+ // 添加文档分隔符
+ return $"---\n{yaml}";
+ }
+
+ ///
+ /// 验证数据完整性
+ ///
+ private List ValidateData(
+ List routes,
+ List clusters)
+ {
+ var results = new List();
+ var clusterIds = clusters.Select(c => c.ClusterId).ToHashSet();
+
+ foreach (var route in routes)
+ {
+ // 检查必填字段
+ if (string.IsNullOrWhiteSpace(route.ServiceName))
+ {
+ results.Add(new ValidationResult
+ {
+ IsValid = false,
+ Entity = $"Route[{route.Id}]",
+ Message = "ServiceName 不能为空"
+ });
+ }
+
+ if (string.IsNullOrWhiteSpace(route.TenantCode))
+ {
+ results.Add(new ValidationResult
+ {
+ IsValid = false,
+ Entity = $"Route[{route.Id}]",
+ Message = "TenantCode 不能为空"
+ });
+ }
+
+ if (string.IsNullOrWhiteSpace(route.ClusterId))
+ {
+ results.Add(new ValidationResult
+ {
+ IsValid = false,
+ Entity = $"Route[{route.Id}]",
+ Message = "ClusterId 不能为空"
+ });
+ }
+
+ // 检查集群引用
+ if (!string.IsNullOrWhiteSpace(route.ClusterId) && !clusterIds.Contains(route.ClusterId))
+ {
+ results.Add(new ValidationResult
+ {
+ IsValid = false,
+ Entity = $"Route[{route.Id}]",
+ Message = $"引用的集群 '{route.ClusterId}' 不存在"
+ });
+ }
+
+ // 检查路径
+ var path = route.GetPath();
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ results.Add(new ValidationResult
+ {
+ IsValid = false,
+ Entity = $"Route[{route.Id}]",
+ Message = "Path 不能为空"
+ });
+ }
+ }
+
+ // 检查集群
+ foreach (var cluster in clusters)
+ {
+ if (string.IsNullOrWhiteSpace(cluster.ClusterId))
+ {
+ results.Add(new ValidationResult
+ {
+ IsValid = false,
+ Entity = $"Cluster[{cluster.Id}]",
+ Message = "ClusterId 不能为空"
+ });
+ }
+
+ var destinations = cluster.GetDestinations();
+ if (destinations.Count == 0)
+ {
+ results.Add(new ValidationResult
+ {
+ IsValid = false,
+ Entity = $"Cluster[{cluster.ClusterId}]",
+ Message = "没有配置目标端点"
+ });
+ }
+
+ foreach (var dest in destinations)
+ {
+ if (string.IsNullOrWhiteSpace(dest.DestinationId))
+ {
+ results.Add(new ValidationResult
+ {
+ IsValid = false,
+ Entity = $"Cluster[{cluster.ClusterId}]/Destination",
+ Message = "DestinationId 不能为空"
+ });
+ }
+
+ if (string.IsNullOrWhiteSpace(dest.Address))
+ {
+ results.Add(new ValidationResult
+ {
+ IsValid = false,
+ Entity = $"Cluster[{cluster.ClusterId}]/Destination[{dest.DestinationId}]",
+ Message = "Address 不能为空"
+ });
+ }
+ }
+ }
+
+ return results;
+ }
+
+ ///
+ /// 保存迁移报告
+ ///
+ private async Task SaveReportAsync(MigrationReport report, CancellationToken cancellationToken)
+ {
+ var reportPath = _options.ReportPath ??
+ Path.Combine(_options.OutputDir, $"migration-report-{DateTime.UtcNow:yyyyMMdd-HHmmss}.json");
+
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+
+ var json = JsonSerializer.Serialize(report, options);
+
+ if (_options.DryRun)
+ {
+ _logger.LogInformation($"[DRY-RUN] 将生成报告: {reportPath}");
+ _logger.LogDebug($"报告内容:\n{json}");
+ }
+ else
+ {
+ await File.WriteAllTextAsync(reportPath, json, cancellationToken);
+ _logger.LogInformation($"已生成报告: {reportPath}");
+ }
+ }
+
+ ///
+ /// 掩盖连接字符串中的敏感信息
+ ///
+ private static string MaskConnectionString(string connectionString)
+ {
+ if (string.IsNullOrEmpty(connectionString))
+ return "[empty]";
+
+ try
+ {
+ var builder = new NpgsqlConnectionStringBuilder(connectionString);
+ if (!string.IsNullOrEmpty(builder.Password))
+ {
+ builder.Password = "***";
+ }
+ return builder.ToString();
+ }
+ catch
+ {
+ return "[invalid connection string]";
+ }
+ }
+}
+
+///
+/// 验证结果
+///
+public class ValidationResult
+{
+ public bool IsValid { get; set; }
+ public string Entity { get; set; } = string.Empty;
+ public string Message { get; set; } = string.Empty;
+}
diff --git a/tools/MigrationTool/MigrationTool.csproj b/tools/MigrationTool/MigrationTool.csproj
new file mode 100644
index 0000000..d6a3c17
--- /dev/null
+++ b/tools/MigrationTool/MigrationTool.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ MigrationTool
+ MigrationTool
+
+
+
+
+
+
+
+
diff --git a/tools/MigrationTool/Models/GatewayConfigModel.cs b/tools/MigrationTool/Models/GatewayConfigModel.cs
new file mode 100644
index 0000000..186a245
--- /dev/null
+++ b/tools/MigrationTool/Models/GatewayConfigModel.cs
@@ -0,0 +1,212 @@
+using System.Text.Json;
+
+namespace MigrationTool.Models;
+
+///
+/// 网关租户路由数据库模型
+///
+public class GwTenantRouteModel
+{
+ public long Id { get; set; }
+ public string TenantCode { get; set; } = string.Empty;
+ public string ServiceName { get; set; } = string.Empty;
+ public string ClusterId { get; set; } = string.Empty;
+ public string PathPattern { get; set; } = string.Empty;
+ public string? MatchJson { get; set; }
+ public int Priority { get; set; }
+ public int Status { get; set; }
+ public bool IsGlobal { get; set; }
+ public bool IsDeleted { get; set; }
+
+ ///
+ /// 解析 Match JSON 获取路径
+ ///
+ public string GetPath()
+ {
+ if (string.IsNullOrEmpty(MatchJson))
+ return PathPattern;
+
+ try
+ {
+ var match = JsonSerializer.Deserialize(MatchJson);
+ return match?.Path ?? PathPattern;
+ }
+ catch
+ {
+ return PathPattern;
+ }
+ }
+
+ ///
+ /// 解析 Match JSON 获取 Host
+ ///
+ public string? GetHost()
+ {
+ if (string.IsNullOrEmpty(MatchJson))
+ return null;
+
+ try
+ {
+ var match = JsonSerializer.Deserialize(MatchJson);
+ return match?.Hosts?.FirstOrDefault();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
+
+///
+/// 路由匹配 JSON 结构
+///
+public class RouteMatchJson
+{
+ public string? Path { get; set; }
+ public List? Methods { get; set; }
+ public List? Hosts { get; set; }
+}
+
+///
+/// 网关集群数据库模型
+///
+public class GwClusterModel
+{
+ public string Id { get; set; } = string.Empty;
+ public string ClusterId { get; set; } = string.Empty;
+ public string Name { get; set; } = string.Empty;
+ public string? Description { get; set; }
+ public string? DestinationsJson { get; set; }
+ public int Status { get; set; }
+ public bool IsDeleted { get; set; }
+
+ ///
+ /// 解析 Destinations JSON
+ ///
+ public List GetDestinations()
+ {
+ if (string.IsNullOrEmpty(DestinationsJson))
+ return [];
+
+ try
+ {
+ var destinations = JsonSerializer.Deserialize>(DestinationsJson);
+ return destinations ?? [];
+ }
+ catch
+ {
+ return [];
+ }
+ }
+}
+
+///
+/// 目标端点模型
+///
+public class GwDestinationModel
+{
+ public string DestinationId { get; set; } = string.Empty;
+ public string Address { get; set; } = string.Empty;
+ public string? Health { get; set; }
+ public int Weight { get; set; } = 1;
+ public int HealthStatus { get; set; } = 1;
+ public int Status { get; set; } = 1;
+}
+
+///
+/// 迁移结果报告
+///
+public class MigrationReport
+{
+ public DateTime StartTime { get; set; }
+ public DateTime EndTime { get; set; }
+ public int TotalRoutes { get; set; }
+ public int SuccessCount { get; set; }
+ public int FailedCount { get; set; }
+ public int SkippedCount { get; set; }
+ public List Entries { get; set; } = [];
+ public TimeSpan Duration => EndTime - StartTime;
+
+ public void PrintSummary()
+ {
+ Console.WriteLine();
+ Console.WriteLine("=".PadRight(60, '='));
+ Console.WriteLine("迁移报告");
+ Console.WriteLine("=".PadRight(60, '='));
+ Console.WriteLine($"开始时间: {StartTime:yyyy-MM-dd HH:mm:ss}");
+ Console.WriteLine($"结束时间: {EndTime:yyyy-MM-dd HH:mm:ss}");
+ Console.WriteLine($"总耗时: {Duration.TotalSeconds:F2} 秒");
+ Console.WriteLine("-".PadRight(60, '-'));
+ Console.WriteLine($"总路由数: {TotalRoutes}");
+ Console.WriteLine($"成功: {SuccessCount}");
+ Console.WriteLine($"失败: {FailedCount}");
+ Console.WriteLine($"跳过: {SkippedCount}");
+ Console.WriteLine("=".PadRight(60, '='));
+
+ if (FailedCount > 0)
+ {
+ Console.WriteLine();
+ Console.WriteLine("失败详情:");
+ foreach (var entry in Entries.Where(e => e.Status == MigrationStatus.Failed))
+ {
+ Console.WriteLine($" - {entry.ServiceName}: {entry.ErrorMessage}");
+ }
+ }
+ }
+}
+
+///
+/// 迁移条目
+///
+public class MigrationEntry
+{
+ public string ServiceName { get; set; } = string.Empty;
+ public string TenantCode { get; set; } = string.Empty;
+ public string ClusterId { get; set; } = string.Empty;
+ public string OutputPath { get; set; } = string.Empty;
+ public MigrationStatus Status { get; set; }
+ public string? ErrorMessage { get; set; }
+}
+
+///
+/// 迁移状态
+///
+public enum MigrationStatus
+{
+ Success,
+ Failed,
+ Skipped
+}
+
+///
+/// K8s Service YAML 模型
+///
+public class K8sServiceModel
+{
+ public string ApiVersion { get; set; } = "v1";
+ public string Kind { get; set; } = "Service";
+ public K8sMetadata Metadata { get; set; } = new();
+ public K8sSpec Spec { get; set; } = new();
+}
+
+public class K8sMetadata
+{
+ public string Name { get; set; } = string.Empty;
+ public Dictionary Labels { get; set; } = new();
+ public Dictionary? Annotations { get; set; }
+}
+
+public class K8sSpec
+{
+ public string Type { get; set; } = "ClusterIP";
+ public Dictionary Selector { get; set; } = new();
+ public List Ports { get; set; } = [];
+}
+
+public class K8sPort
+{
+ public int Port { get; set; }
+ public int TargetPort { get; set; }
+ public string? Name { get; set; }
+ public string? Protocol { get; set; }
+}
diff --git a/tools/MigrationTool/Program.cs b/tools/MigrationTool/Program.cs
new file mode 100644
index 0000000..a7224ee
--- /dev/null
+++ b/tools/MigrationTool/Program.cs
@@ -0,0 +1,318 @@
+using System.Text;
+using MigrationTool;
+using MigrationTool.Models;
+
+// 设置控制台输出编码
+Console.OutputEncoding = Encoding.UTF8;
+
+PrintBanner();
+
+// 解析命令行参数
+var options = ParseArguments(args);
+
+if (options == null)
+{
+ PrintHelp();
+ return 1;
+}
+
+PrintOptions(options);
+
+// 确认执行
+if (!options.DryRun)
+{
+ Console.WriteLine();
+ Console.Write("确认开始迁移? (y/N): ");
+ var confirm = Console.ReadLine()?.Trim().ToLowerInvariant();
+ if (confirm != "y" && confirm != "yes")
+ {
+ Console.WriteLine("已取消");
+ return 0;
+ }
+}
+
+Console.WriteLine();
+
+// 创建日志记录器
+var logger = new ConsoleLogger(options.LogLevel);
+
+// 执行迁移
+try
+{
+ var service = new MigrationService(options, logger);
+ var report = await service.MigrateAsync();
+
+ // 打印报告
+ report.PrintSummary();
+
+ return report.FailedCount > 0 ? 1 : 0;
+}
+catch (Exception ex)
+{
+ logger.LogError($"迁移失败: {ex.Message}");
+ logger.LogDebug(ex.StackTrace ?? "");
+ return 2;
+}
+
+///
+/// 解析命令行参数
+///
+static MigrationOptions? ParseArguments(string[] args)
+{
+ var options = new MigrationOptions
+ {
+ ConnectionString = GetDefaultConnectionString()
+ };
+
+ for (int i = 0; i < args.Length; i++)
+ {
+ var arg = args[i];
+ switch (arg.ToLowerInvariant())
+ {
+ case "--help":
+ case "-h":
+ case "-?":
+ return null;
+
+ case "--connection-string":
+ case "-c":
+ if (i + 1 < args.Length)
+ options.ConnectionString = args[++i];
+ break;
+
+ case "--output-dir":
+ case "-o":
+ if (i + 1 < args.Length)
+ options.OutputDir = args[++i];
+ break;
+
+ case "--dry-run":
+ case "-d":
+ options.DryRun = true;
+ break;
+
+ case "--default-host":
+ if (i + 1 < args.Length)
+ options.DefaultHost = args[++i];
+ break;
+
+ case "--service-port":
+ if (i + 1 < args.Length && int.TryParse(args[++i], out var svcPort))
+ options.ServicePort = svcPort;
+ break;
+
+ case "--target-port":
+ if (i + 1 < args.Length && int.TryParse(args[++i], out var tgtPort))
+ options.TargetPort = tgtPort;
+ break;
+
+ case "--no-validate":
+ options.Validate = false;
+ break;
+
+ case "--tenant":
+ case "-t":
+ if (i + 1 < args.Length)
+ options.TenantCode = args[++i];
+ break;
+
+ case "--log-level":
+ case "-l":
+ if (i + 1 < args.Length && Enum.TryParse(args[++i], true, out var level))
+ options.LogLevel = level;
+ break;
+
+ case "--no-report":
+ options.GenerateReport = false;
+ break;
+
+ case "--report-path":
+ if (i + 1 < args.Length)
+ options.ReportPath = args[++i];
+ break;
+
+ default:
+ Console.WriteLine($"未知参数: {arg}");
+ break;
+ }
+ }
+
+ return options;
+}
+
+///
+/// 获取默认连接字符串(从环境变量)
+///
+static string GetDefaultConnectionString()
+{
+ var envConnectionString = Environment.GetEnvironmentVariable("GATEWAY_CONNECTION_STRING");
+ if (!string.IsNullOrEmpty(envConnectionString))
+ {
+ return envConnectionString;
+ }
+
+ return "Host=localhost;Database=fengling_gateway;Username=postgres;Password=postgres";
+}
+
+///
+/// 打印 Banner
+///
+static void PrintBanner()
+{
+ Console.WriteLine();
+ Console.WriteLine(@" __ __ _ _ _ _ _ _ ");
+ Console.WriteLine(@" | \/ (_) | ___ _ __ | |_(_)_ __ __ _| |_ ___ | |_ ");
+ Console.WriteLine(@" | |\/| | | |/ _ \ '_ \| __| | '_ \ / _` | __/ _ \ | __|");
+ Console.WriteLine(@" | | | | | | __/ | | | |_| | | | | (_| | || __/ | |_ ");
+ Console.WriteLine(@" |_| |_|_|_|\___|_| |_|\__|_|_| |_|\__,_|\__\___| \__|");
+ Console.WriteLine(@" ");
+ Console.WriteLine(@" Fengling Gateway Migration Tool v1.0.0");
+ Console.WriteLine();
+}
+
+///
+/// 打印帮助信息
+///
+static void PrintHelp()
+{
+ Console.WriteLine("用法: MigrationTool [选项]");
+ Console.WriteLine();
+ Console.WriteLine("选项:");
+ Console.WriteLine(" -h, --help 显示帮助信息");
+ Console.WriteLine(" -c, --connection-string 数据库连接字符串 (默认: 从 GATEWAY_CONNECTION_STRING 环境变量读取)");
+ Console.WriteLine(" -o, --output-dir YAML 文件输出目录 (默认: ./output)");
+ Console.WriteLine(" -d, --dry-run 干运行模式,只输出不写入文件");
+ Console.WriteLine(" --default-host 默认路由 Host (默认: api.fengling.com)");
+ Console.WriteLine(" --service-port Service 端口 (默认: 80)");
+ Console.WriteLine(" --target-port 目标端口 (默认: 8080)");
+ Console.WriteLine(" --no-validate 跳过数据验证");
+ Console.WriteLine(" -t, --tenant 仅处理指定租户");
+ Console.WriteLine(" -l, --log-level 日志级别 (Trace/Debug/Information/Warning/Error)");
+ Console.WriteLine(" --no-report 不生成报告文件");
+ Console.WriteLine(" --report-path 指定报告文件路径");
+ Console.WriteLine();
+ Console.WriteLine("示例:");
+ Console.WriteLine(" MigrationTool --dry-run");
+ Console.WriteLine(" MigrationTool -c \"Host=db;Database=gateway;Username=postgres;Password=secret\" -o ./yaml");
+ Console.WriteLine(" MigrationTool --tenant tenant1 --dry-run");
+ Console.WriteLine();
+}
+
+///
+/// 打印选项
+///
+static void PrintOptions(MigrationOptions options)
+{
+ Console.WriteLine("配置选项:");
+ Console.WriteLine($" 连接字符串: {MaskConnectionString(options.ConnectionString)}");
+ Console.WriteLine($" 输出目录: {Path.GetFullPath(options.OutputDir)}");
+ Console.WriteLine($" Dry-Run: {(options.DryRun ? "是" : "否")}");
+ Console.WriteLine($" 默认 Host: {options.DefaultHost}");
+ Console.WriteLine($" Service 端口: {options.ServicePort}");
+ Console.WriteLine($" Target 端口: {options.TargetPort}");
+ Console.WriteLine($" 验证数据: {(options.Validate ? "是" : "否")}");
+ Console.WriteLine($" 日志级别: {options.LogLevel}");
+ if (!string.IsNullOrEmpty(options.TenantCode))
+ {
+ Console.WriteLine($" 指定租户: {options.TenantCode}");
+ }
+}
+
+///
+/// 掩盖连接字符串中的敏感信息
+///
+static string MaskConnectionString(string connectionString)
+{
+ if (string.IsNullOrEmpty(connectionString))
+ return "[empty]";
+
+ try
+ {
+ var builder = new Npgsql.NpgsqlConnectionStringBuilder(connectionString);
+ if (!string.IsNullOrEmpty(builder.Password))
+ {
+ builder.Password = "***";
+ }
+ return builder.ToString();
+ }
+ catch
+ {
+ return "[invalid]";
+ }
+}
+
+///
+/// 简单的控制台日志记录器
+///
+public class ConsoleLogger : ILogger
+{
+ private readonly LogLevel _minLevel;
+
+ public ConsoleLogger(LogLevel minLevel)
+ {
+ _minLevel = minLevel;
+ }
+
+ public void LogTrace(string message)
+ {
+ if (_minLevel <= LogLevel.Trace)
+ WriteLog("TRC", ConsoleColor.Gray, message);
+ }
+
+ public void LogDebug(string message)
+ {
+ if (_minLevel <= LogLevel.Debug)
+ WriteLog("DBG", ConsoleColor.DarkGray, message);
+ }
+
+ public void LogInformation(string message)
+ {
+ if (_minLevel <= LogLevel.Information)
+ WriteLog("INF", ConsoleColor.White, message);
+ }
+
+ public void LogWarning(string message)
+ {
+ if (_minLevel <= LogLevel.Warning)
+ WriteLog("WRN", ConsoleColor.Yellow, message);
+ }
+
+ public void LogError(string message)
+ {
+ if (_minLevel <= LogLevel.Error)
+ WriteLog("ERR", ConsoleColor.Red, message);
+ }
+
+ private static void WriteLog(string level, ConsoleColor color, string message)
+ {
+ var timestamp = DateTime.Now.ToString("HH:mm:ss");
+ var originalColor = Console.ForegroundColor;
+ Console.ForegroundColor = color;
+ Console.WriteLine($"[{timestamp}] [{level}] {message}");
+ Console.ForegroundColor = originalColor;
+ }
+}
+
+///
+/// 日志记录器接口
+///
+public interface ILogger
+{
+ void LogTrace(string message);
+ void LogDebug(string message);
+ void LogInformation(string message);
+ void LogWarning(string message);
+ void LogError(string message);
+}
+
+///
+/// 日志级别
+///
+public enum LogLevel
+{
+ Trace,
+ Debug,
+ Information,
+ Warning,
+ Error
+}
diff --git a/网关配置的新想法.md b/网关配置的新想法.md
new file mode 100644
index 0000000..e2f1660
--- /dev/null
+++ b/网关配置的新想法.md
@@ -0,0 +1,23 @@
+#### 网关配置的新想法
+
+路由/集群/目标 等配置还是通过数据库变更通知网关进行重新加载的方式触发变更
+
+k8s 的service中需要有固定的label来约定产生新的配置
+以下是范例
+service-label
+ - app-router-host = https://hostname #代表网关域名地址
+ - app-router-name = member # 代表路由名
+ - app-router-prefix = /member # 代表路由前缀
+ - app-cluster-name = member #代表集群名(id)
+ - app-cluster-destination = default # 代表标准服务目标 如果不是default 比如 1668 则代表企业编号独有的目标
+详细请求说明:
+比如一个请求进来了 请求路径是 {host}/member/api/v1/memberinfo/{id} header:{authorization: bearer xxx}
+-> 根据 host+ prefix匹配到 member路由 -> 进入到对应的cluster ->
+中间件进行解析或者是yarp-transform能做到最好使用yarp-transform来处理 或者如果我集成了openiddict是否可以拿到jwt这部分的信息
+-> 解析出 customer/或者是租户id都行 只要能表示租户的都算 -> 找到对应的租户就进对应的目标服务 找不到就进 标准服务目标
+
+配置生效详细说明:
+1.在console项目中监听k8s 的服务 如果发现有新的app-router-name 相关信息就要产生待执行配置 用户明确确认后 生成数据库配置 在此之前只能存在于内存/缓存中 ;
+2.同样的监听服务 发现app-cluster-name 检查是否有新的app-cluster-name 如果有新的 同样产生待执行配置 后续于路由一致
+3.同样监听服务 发现app-cluster-destination 检查对应的cluster是否存在这个destination 如果不存在 同样产生待执行配置 后续与上述一致
+4.用户确认是否执行这些配置 用户加载到界面后可调整配置,同意后产生持久化数据到数据库,点击立即生效才下发到yarp网关 网关重新根据新的配置加载到最新配置后生效
\ No newline at end of file