Skip to main content

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
LoggingSerilog 結構化記錄組態
Settings泛型 JSON 檔案持久化設定服務

專案參照關係:

GST.Rpa.Infrastructure → GST.Rpa.Core

NuGet 引用與相依

以下版本由 Directory.Packages.props 集中管理:

Package版本用途
Metalama.Framework2025.1.16AOP 編譯期織入
Serilog4.3.0結構化記錄核心
Serilog.Extensions.Hosting8.0.0Host 整合
Serilog.Settings.Configuration8.0.4讀取 IConfiguration 設定
Serilog.Sinks.Console6.1.1Console 輸出
Serilog.Sinks.File7.0.0檔案輸出(每日滾動)
System.Management8.0.0WMI 硬體資訊查詢

另外參照 .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;
}

授權層級矩陣

層級啟用功能期限硬體綁定
TrialElementScanning | ValueMonitoring30 天
StandardTrial + BasicControl年授權
ProfessionalStandard + 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 欄位讀取 LicenseTypeFeaturesIssuedToIssuedDate
  • 過期檢查 — 支援 Expiration_Date_Lock_EnableEvaluation_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)
  • 例外時:完整堆疊追蹤、SourceTargetSite、遞迴 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
}
}
閾值等級預設值
< WarningThresholdMsDebug< 1000 ms
>= WarningThresholdMsWarning>= 1000 ms
>= ErrorThresholdMsError>= 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);
}

雙模式運作:

  1. IConfiguration — 從設定檔讀取完整 Serilog 組態(loggerConfig.ReadFrom.Configuration(configuration)
  2. 無設定檔(後備) — 使用硬編碼預設值

預設 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 序列化選項:

選項
WriteIndentedtrue
PropertyNamingPolicyJsonNamingPolicy.CamelCase
DefaultIgnoreConditionWhenWritingNull
自訂 ConverterRectangleJsonConverterColorJsonConverterJsonStringEnumConverter

行為特性:

  • 檔案不存在時自動建立預設設定檔
  • 讀取失敗時降級為 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 而非執行期配置切換,原因:

  1. 安全性 — Release 組建中完全不包含 DevelopmentLicenseService 的程式碼路徑
  2. 相依性ReactorLicenseService 依賴 .NET Reactor SDK DLL;開發機不需安裝
  3. 簡潔性 — 無需額外的設定檔或 feature flag 機制

分散式 Worker Pool 架構(Planned)

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.02026-04-04初版