GST.Rpa.Infrastructure — 基礎設施
- 適用對象: GST 內部開發人員
- 前置知識: C# / .NET 8 / DI / AOP 概念
- 閱讀時間: 20 分鐘
- 最後更新: 2026-04-04
- 版本: 1.0.0
模組職責
GST.Rpa.Infrastructure 負責所有 cross-cutting concerns,讓上層模組(Core、Automation、App)專注於業務邏輯。職責範圍包含:
| 子系統 | 說明 |
|---|---|
| Licensing | 授權驗證與功能閘門控制 |
| AOP Aspects | 透過 Metalama 自動注入 logging / performance / exception handling |
| Logging | Serilog 結構化記錄組態 |
| Settings | 泛型 JSON 檔案持久化設定服務 |
專案參照關係:
GST.Rpa.Infrastructure → GST.Rpa.Core
NuGet 引用與相依
以下版本由 Directory.Packages.props 集中管理:
| Package | 版本 | 用途 |
|---|---|---|
Metalama.Framework | 2025.1.16 | AOP 編譯期織入 |
Serilog | 4.3.0 | 結構化記錄核心 |
Serilog.Extensions.Hosting | 8.0.0 | Host 整合 |
Serilog.Settings.Configuration | 8.0.4 | 讀取 IConfiguration 設定 |
Serilog.Sinks.Console | 6.1.1 | Console 輸出 |
Serilog.Sinks.File | 7.0.0 | 檔案輸出(每日滾動) |
System.Management | 8.0.0 | WMI 硬體資訊查詢 |
另外參照 .NET Reactor SDK(本機 DLL):
<Reference Include="License">
<HintPath>...\Eziriz\.NET Reactor\SDK\Binaries\License.dll .NET 6\License.dll</HintPath>
</Reference>
License 系統
ILicenseService 介面
定義於 GST.Rpa.Core.Abstractions,是授權系統的抽象契約:
public interface ILicenseService
{
LicenseInfo GetLicenseInfo();
bool ValidateLicense();
bool IsFeatureEnabled(LicenseFeature feature);
LicenseActivationResult Activate(string licenseKey);
LicenseActivationResult ActivateFromFile(string filePath);
bool Deactivate();
string GetHardwareId();
}
LicenseType 列舉
public enum LicenseType
{
Trial, // 30 天試用
Standard, // 基礎功能
Professional, // 進階功能
Enterprise // 全功能 + API
}
LicenseStatus 列舉
public enum LicenseStatus
{
Valid, // 有效
Expired, // 已過期
TrialExpired, // 試用到期
HardwareMismatch, // 硬體 ID 不符
Invalid, // 檔案損毀或無效
NotActivated, // 未啟用
GracePeriod // 過期但在 7 天寬限期內
}
LicenseFeature Flags 列舉
[Flags]
public enum LicenseFeature
{
None = 0,
ElementScanning = 1 << 0, // 元素掃描
ValueMonitoring = 1 << 1, // 數值監控
BasicControl = 1 << 2, // 基礎控制(click, set value)
AdvancedControl = 1 << 3, // 進階控制(drag-drop, scroll, send keys)
ScriptRecording = 1 << 4, // 腳本錄製
ScriptPlayback = 1 << 5, // 腳本播放
BatchExecution = 1 << 6, // 批次執行
ApiAccess = 1 << 7, // API 存取
All = 0xFF // 所有功能
}
LicenseInfo Record
public record LicenseInfo(
LicenseType LicenseType,
LicenseStatus Status,
string IssuedTo,
DateTime IssuedDate,
DateTime? ExpirationDate, // null = 永久授權
string HardwareId,
LicenseFeature EnabledFeatures
)
{
public bool IsValid => Status is LicenseStatus.Valid or LicenseStatus.GracePeriod;
public int? DaysRemaining => ExpirationDate.HasValue
? Math.Max(0, (ExpirationDate.Value - DateTime.Now).Days) : null;
public bool IsTrial => LicenseType == LicenseType.Trial;
}
授權層級矩陣
| 層級 | 啟用功能 | 期限 | 硬體綁定 |
|---|---|---|---|
| Trial | ElementScanning | ValueMonitoring | 30 天 | 否 |
| Standard | Trial + BasicControl | 年授權 | 是 |
| Professional | Standard + AdvancedControl | ScriptRecording | ScriptPlayback | 年授權 | 是 |
| Enterprise | 所有功能(All = 0xFF),含 BatchExecution | ApiAccess | 永久或年授權 | 是 |
ReactorLicenseService(Production)
ReactorLicenseService 整合 .NET Reactor 授權 SDK,用於 Release 組建:
- 硬體綁定 — 透過
Status.GetHardwareID(Board, CPU, HDD, MAC: false)產生機器指紋 - 授權載入 — 使用
Status.LoadLicense(filePath)讀取.license檔案 - KeyValueList — 從授權檔案的 KeyValue 欄位讀取
LicenseType、Features、IssuedTo、IssuedDate - 過期檢查 — 支援
Expiration_Date_Lock_Enable及Evaluation_Lock_Enabled(試用天數) - 寬限期 — 過期後額外 7 天寬限期(
GracePeriod狀態) - 啟用失效 —
Status.InvalidateLicense()產生確認碼
public class ReactorLicenseService : ILicenseService
{
private readonly ILogger<ReactorLicenseService> _logger;
private LicenseInfo? _currentLicense;
public ReactorLicenseService(ILogger<ReactorLicenseService> logger) { ... }
public LicenseInfo GetLicenseInfo() { ... }
public bool ValidateLicense() { ... }
public bool IsFeatureEnabled(LicenseFeature feature) { ... }
public LicenseActivationResult Activate(string licenseKey) { ... }
public LicenseActivationResult ActivateFromFile(string filePath) { ... }
public bool Deactivate() { ... }
public string GetHardwareId()
=> Status.GetHardwareID(Board: true, CPU: true, HDD: true, MAC: false);
}
DevelopmentLicenseService(Development)
開發模式下的授權服務,所有功能永久啟用,免除授權驗證:
public class DevelopmentLicenseService : ILicenseService
{
public LicenseInfo GetLicenseInfo()
=> new(LicenseType.Enterprise, LicenseStatus.Valid,
"Developer", DateTime.Now.AddYears(-1),
null, GetHardwareId(), LicenseFeature.All);
public bool ValidateLicense() => true;
public bool IsFeatureEnabled(LicenseFeature feature) => true;
// Activate / Deactivate 直接回傳成功
}
硬體 ID 透過 WMI(System.Management)查詢 CPU、主機板、BIOS 序號後計算 SHA256 雜湊:
// Win32_Processor → ProcessorId
// Win32_BaseBoard → SerialNumber
// Win32_BIOS → SerialNumber
// 組合後 SHA256
條件式編譯切換
在 ServiceCollectionExtensions.AddRpaInfrastructure() 中透過 #if DEBUG 切換實作:
#if DEBUG
services.AddSingleton<ILicenseService, DevelopmentLicenseService>();
#else
services.AddSingleton<ILicenseService, ReactorLicenseService>();
#endif
- DEBUG 組建 →
DevelopmentLicenseService(全功能、無限制) - Release 組建 →
ReactorLicenseService(.NET Reactor 硬體綁定驗證)
Metalama AOP Aspects
所有 Aspect 均繼承 OverrideMethodAspect,在編譯期由 Metalama 織入目標方法。
LoggingAspect
自動記錄方法進入 / 離開 / 例外,含執行時間:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class LoggingAspect : OverrideMethodAspect
{
public override dynamic? OverrideMethod()
{
// 1. 從目標類別取得 _logger field
// 2. LogInformation("Entering {Method}", methodName)
// 3. Stopwatch 計時
// 4. meta.Proceed() 執行原始方法
// 5. LogInformation("Exiting {Method} | Duration: {Duration}ms", ...)
// 6. catch → LogError 包含完整 StackTrace + InnerException 鏈
}
}
記錄內容:
- 方法名稱、執行時間(ms)
- 例外時:完整堆疊追蹤、
Source、TargetSite、遞迴InnerException
PerformanceAspect
效能監控,超過閾值時自動告警:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class PerformanceAspect : OverrideMethodAspect
{
public long WarningThresholdMs { get; set; } = 1000; // 預設 1 秒
public long ErrorThresholdMs { get; set; } = 5000; // 預設 5 秒
public override dynamic? OverrideMethod()
{
// Stopwatch 計時 → meta.Proceed()
// >= ErrorThresholdMs → LogError
// >= WarningThresholdMs → LogWarning
// 其他 → LogDebug
}
}
| 閾值 | 等級 | 預設值 |
|---|---|---|
< WarningThresholdMs | Debug | < 1000 ms |
>= WarningThresholdMs | Warning | >= 1000 ms |
>= ErrorThresholdMs | Error | >= 5000 ms |
RequiresLicenseAspect
方法層級的授權功能閘門:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class RequiresLicenseAspect : OverrideMethodAspect
{
public string RequiredFeatureName { get; set; } = string.Empty;
public override dynamic? OverrideMethod()
{
// 1. 從目標類別取得 _licenseService field
// 2. 將 RequiredFeatureName 解析為 LicenseFeature enum
// 3. 呼叫 licenseService.IsFeatureEnabled(feature)
// 4. 若未授權 → throw FeatureNotLicensedException(feature)
// 5. 已授權 → meta.Proceed()
}
}
使用方式:
[RequiresLicenseAspect(RequiredFeatureName = "ScriptRecording")]
public void StartRecording() { ... }
RequiresLicenseAspect 要求目標類別包含 _licenseService 欄位(型別為 ILicenseService)。
ExceptionHandlingAspect
統一例外記錄,不吞例外(re-throw):
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ExceptionHandlingAspect : OverrideMethodAspect
{
public override dynamic? OverrideMethod()
{
try { return meta.Proceed(); }
catch (Exception ex)
{
// 從 _logger field 取得 logger
// LogError("Unhandled exception in {Method}: {Message}", ...)
throw; // 重新拋出
}
}
}
RpaServiceFabric — 自動織入
RpaServiceFabric 繼承 TransitiveProjectFabric,在編譯期自動將 Aspect 套用到所有 FlaUI 服務類別:
public class RpaServiceFabric : TransitiveProjectFabric
{
public override void AmendProject(IProjectAmender amender)
{
// 選取所有 FlaUI 服務(Scanner / Monitor / Controller)
var flaUiMethods = amender
.SelectMany(c => c.AllTypes)
.Where(t => t.Name.StartsWith("FlaUI") &&
!t.Name.EndsWith("Tests") &&
(t.Name.Contains("Scanner") ||
t.Name.Contains("Monitor") ||
t.Name.Contains("Controller")))
.SelectMany(t => t.Methods)
.Where(m => !m.IsAbstract);
// 套用 LoggingAspect(除 [NoLog])
flaUiMethods
.Where(m => !m.Attributes.Any(a => a.Type.Name == "NoLogAttribute"))
.AddAspect<LoggingAspect>();
// 套用 PerformanceAspect(除 [NoPerformance])
flaUiMethods
.Where(m => !m.Attributes.Any(a => a.Type.Name == "NoPerformanceAttribute"))
.AddAspect<PerformanceAspect>();
// 套用 ExceptionHandlingAspect(除 [NoExceptionHandling])
flaUiMethods
.Where(m => !m.Attributes.Any(a => a.Type.Name == "NoExceptionHandlingAttribute"))
.AddAspect<ExceptionHandlingAspect>();
// RequiresLicenseAspect 需手動標註(需指定 RequiredFeatureName)
}
}
自動套用對象:
| 類別名稱匹配 | 套用的 Aspects |
|---|---|
FlaUI*Scanner* | Logging + Performance + ExceptionHandling |
FlaUI*Monitor* | Logging + Performance + ExceptionHandling |
FlaUI*Controller* | Logging + Performance + ExceptionHandling |
Opt-out Attributes(排除標記):
[NoLog] // 排除 LoggingAspect
[NoPerformance] // 排除 PerformanceAspect
[NoExceptionHandling] // 排除 ExceptionHandlingAspect
Serilog 結構化記錄
SerilogConfiguration
public static class SerilogConfiguration
{
public static LoggerConfiguration ConfigureSerilog(IConfiguration? configuration = null);
public static ILogger CreateLogger(IConfiguration? configuration = null);
}
雙模式運作:
- 有
IConfiguration— 從設定檔讀取完整 Serilog 組態(loggerConfig.ReadFrom.Configuration(configuration)) - 無設定檔(後備) — 使用硬編碼預設值
預設 Sink 配置:
| Sink | 輸出格式 | 設定 |
|---|---|---|
| Console | [HH:mm:ss LVL] Message | — |
| File | [yyyy-MM-dd HH:mm:ss.fff] [LVL] [SourceContext] Message | 每日滾動、保留 30 天 |
檔案路徑: %APPDATA%\GST.Rpa\logs\app-{date}.log
最小等級覆蓋:
MinimumLevel: Information
Override "Microsoft": Warning
Override "System": Warning
Enrich: FromLogContext
Settings 服務
ISettingsService<T> 介面
public interface ISettingsService<T> where T : class, new()
{
Task<T> LoadAsync();
Task SaveAsync(T settings);
Task ResetAsync();
T GetCurrent();
event EventHandler<T>? SettingsChanged;
}
JsonSettingsService<T> 實作
泛型 JSON 檔案持久化服務,儲存於 %APPDATA%\GST.Rpa\:
public class JsonSettingsService<T> : ISettingsService<T> where T : class, new()
{
public JsonSettingsService(string fileName, ILogger<JsonSettingsService<T>> logger);
public async Task<T> LoadAsync(); // 讀取 JSON → 反序列化
public async Task SaveAsync(T settings); // 序列化 → 寫入 JSON
public async Task ResetAsync(); // 重設為 new T()
public T GetCurrent(); // 同步取得快取值
public event EventHandler<T>? SettingsChanged;
}
JSON 序列化選項:
| 選項 | 值 |
|---|---|
WriteIndented | true |
PropertyNamingPolicy | JsonNamingPolicy.CamelCase |
DefaultIgnoreCondition | WhenWritingNull |
| 自訂 Converter | RectangleJsonConverter、ColorJsonConverter、JsonStringEnumConverter |
行為特性:
- 檔案不存在時自動建立預設設定檔
- 讀取失敗時降級為
new T()預設值(不拋例外) SaveAsync完成後觸發SettingsChanged事件GetCurrent()若快取為空則同步載入
使用範例
DI 註冊
public static IServiceCollection AddRpaInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
// Serilog 記錄
services.AddLogging(builder =>
{
builder.ClearProviders();
builder.AddSerilog(SerilogConfiguration.CreateLogger(configuration));
});
// 授權服務(條件式編譯)
#if DEBUG
services.AddSingleton<ILicenseService, DevelopmentLicenseService>();
#else
services.AddSingleton<ILicenseService, ReactorLicenseService>();
#endif
// Settings 服務
services.AddSingleton<ISettingsService<ElementPickerSettings>>(sp =>
{
var logger = sp.GetRequiredService<ILogger<JsonSettingsService<ElementPickerSettings>>>();
return new JsonSettingsService<ElementPickerSettings>(
"element-picker-settings.json", logger);
});
return services;
}
Aspect 標註範例
public class MyRpaService
{
private readonly ILogger<MyRpaService> _logger; // LoggingAspect 需要
private readonly ILicenseService _licenseService; // RequiresLicenseAspect 需要
[RequiresLicenseAspect(RequiredFeatureName = "ScriptRecording")]
public void StartRecording()
{
// 授權檢查由 Aspect 自動處理
// 未授權時拋出 FeatureNotLicensedException
}
[NoLog] // 排除自動 logging
public void FrequentPolling()
{
// 高頻呼叫不需要逐次記錄
}
}
架構決策
為何選擇 Metalama(而非手動裝飾器)
| 考量 | 手動裝飾器模式 | Metalama AOP |
|---|---|---|
| 重複程式碼 | 每個方法都要寫 try/catch + stopwatch | 零重複,Aspect 統一處理 |
| 漏標風險 | 新增方法容易忘記加入 | Fabric 自動套用到所有符合條件的方法 |
| 維護成本 | 修改記錄格式需改 N 處 | 只改 Aspect 一處 |
| 編譯期 vs 執行期 | — | Metalama 在編譯期織入,無反射效能損耗 |
| Opt-out | 不適用 | [NoLog]、[NoPerformance] 等標記可精細排除 |
條件式編譯的授權切換
採用 #if DEBUG 而非執行期配置切換,原因:
- 安全性 — Release 組建中完全不包含
DevelopmentLicenseService的程式碼路徑 - 相依性 —
ReactorLicenseService依賴 .NET Reactor SDK DLL;開發機不需安裝 - 簡潔性 — 無需額外的設定檔或 feature flag 機制
分散式 Worker Pool 架構(Planned)
目前 GST.Rpa 為單機執行:RpaEngine 在主機本機跑 Workflow,靠 Infrastructure 提供 License / Log / Settings。當需要橫向擴展(同時跑多個 Workflow、跨機器佈署)時,規劃以下分散式 Worker Pool 架構:
各角色職責:
- Coordinator:唯一派工點,維護 Worker 註冊表 + 心跳追蹤;偵測心跳逾時時將 in-flight 工作重新入列
- Job Queue:解耦派工與執行;用 Redis Stream / RabbitMQ 的 ACK 機制保證 at-least-once delivery
- Worker Node:本身就是現有的單機 Engine(Core + Automation),多加一個 Job Consumer wrapper 即可;無狀態,可隨時擴縮
- Result Store:保存執行結果與截圖(PostgreSQL 存 metadata、Blob 存大型 artifact)
失敗模式與處理
| 故障 | Coordinator 行為 | Worker 行為 |
|---|---|---|
| Worker 崩潰 | 心跳逾時 → 將該 Worker 的 in-flight Job 重新入列 | — |
| Job 執行失敗 | 收到 NACK → 依重試策略決定再試或標記失敗 | 拋 WorkflowException → NACK |
| Coordinator 重啟 | 從 Result Store 還原狀態繼續服務 | 心跳改寫到新 Coordinator instance |
| Queue 不可用 | 進入降級模式,本機 fallback | 改為輪詢本機 Job 檔案 |
實作優先順序與 issue 追蹤待後續 RFC 確認。
版本紀錄
| 版本 | 日期 | 說明 |
|---|---|---|
| 1.0 | 2026-04-04 | 初版 |