GST.Rpa.Web — Blazor 控制台
| 項目 | 內容 |
|---|---|
| 適用對象 | GST 內部開發人員 |
| 前置知識 | C# / .NET 8 / Blazor Server / SignalR |
| 閱讀時間 | 20 分鐘 |
| 最後更新 | 2026-04-04 |
| 版本 | 1.0.0 |
1. 模組職責
GST.Rpa.Web 是 RPA 系統的 Web-based 控制台,以 Blazor Server 建構,提供:
- Monaco Editor — 嵌入式 C# 腳本編輯器,支援 breakpoint glyph、error marker 及當前行高亮
- DOT Graph 視覺化 — 透過 D3-graphviz 即時渲染 workflow 狀態機圖形,標記 current / visited 狀態
- 即時狀態同步 — 經由 SignalR 接收 Engine 的 state change、log、screen capture 及 variable snapshot
- 偵錯控制 — Pause / Resume / StepOver / Continue / Abort 及 Breakpoint 管理
專案結構:
GST.Rpa.Web/
├── Interop/
│ ├── MonacoInterop.cs ← Monaco Editor JS 互動
│ └── GraphvizInterop.cs ← D3-graphviz DOT 渲染
├── Models/
│ ├── ConsoleLogEntry.cs ← 日誌項目
│ ├── TimelineStep.cs ← 工作流程步驟追蹤
│ └── BreakpointInfo.cs ← 中斷點描述
├── Services/
│ ├── IRpaConsoleStateService.cs ← 核心介面
│ ├── SignalRConsoleStateService.cs ← 正式 SignalR 實作
│ ├── MockConsoleStateService.cs ← 開發用 Mock 實作
│ ├── RpaWebOptions.cs ← 設定選項
│ └── ServiceCollectionExtensions.cs ← DI 註冊
└── Program.cs
2. NuGet 引用與相依
| 套件 | 版本 | 用途 |
|---|---|---|
BlazorMonaco | 3.3.0 | Monaco Editor Blazor 封裝 |
Microsoft.AspNetCore.SignalR.Client | 8.0.11 | SignalR Client — 連線 RPA Hub |
專案同時引用內部模組:
<ProjectReference Include="..\GST.Rpa.Core\GST.Rpa.Core.csproj" />
<ProjectReference Include="..\GST.Rpa.SignalR\GST.Rpa.SignalR.csproj" />
<ProjectReference Include="..\GST.Rpa.Scripting\GST.Rpa.Scripting.csproj" />
- GST.Rpa.Core — 提供
RpaEngineStateenum 等共用型別 - GST.Rpa.SignalR — SignalR Hub 定義
- GST.Rpa.Scripting — C# 腳本引擎
3. 核心 API
3.1 IRpaConsoleStateService
控制台的核心抽象,實作 IAsyncDisposable。所有 Blazor 元件透過此介面取得即時狀態與發送控制指令。
public interface IRpaConsoleStateService : IAsyncDisposable
{
// ── State Properties ──
string? CurrentStateName { get; }
string? DotGraph { get; }
byte[]? ScreenFrame { get; }
RpaEngineState EngineState { get; }
IReadOnlyList<ConsoleLogEntry> LogEntries { get; }
IReadOnlyList<TimelineStep> TimelineSteps { get; }
IReadOnlyDictionary<string, object?> Variables { get; }
IReadOnlySet<int> Breakpoints { get; }
// ── Event ──
event Action? OnChange;
// ── Control Methods ──
Task ExecuteScriptAsync(string code, CancellationToken cancellationToken = default);
Task PauseAsync(CancellationToken cancellationToken = default);
Task ResumeAsync(CancellationToken cancellationToken = default);
Task AbortAsync(CancellationToken cancellationToken = default);
Task SetBreakpointAsync(int lineNumber, CancellationToken cancellationToken = default);
Task RemoveBreakpointAsync(int lineNumber, CancellationToken cancellationToken = default);
Task StepOverAsync(CancellationToken cancellationToken = default);
Task ContinueAsync(CancellationToken cancellationToken = default);
Task ConnectAsync(CancellationToken cancellationToken = default);
}
| 屬性 | 型別 | 說明 |
|---|---|---|
CurrentStateName | string? | 目前 State Machine 狀態名稱 |
DotGraph | string? | 當前 workflow 的 DOT 語言圖形 |
ScreenFrame | byte[]? | 最新螢幕截圖 raw bytes |
EngineState | RpaEngineState | 引擎執行狀態(Idle / Running / Paused / Error / Completed) |
LogEntries | IReadOnlyList<ConsoleLogEntry> | 執行期間產生的日誌(最多 500 筆,超過後移除最早的) |
TimelineSteps | IReadOnlyList<TimelineStep> | workflow 各步驟及執行狀態 |
Variables | IReadOnlyDictionary<string, object?> | 腳本變數快照 |
Breakpoints | IReadOnlySet<int> | 已設定的中斷點行號集合 |
3.2 SignalRConsoleStateService
正式環境的 IRpaConsoleStateService 實作,透過 SignalR HubConnection 與 RPA Engine 即時通訊。
public class SignalRConsoleStateService : IRpaConsoleStateService
{
public SignalRConsoleStateService(RpaWebOptions options, NavigationManager navigation)
// ...
}
連線建立:
ConnectAsync() 使用 RpaWebOptions.SignalRHubUrl 建立 Hub 連線,並啟用 WithAutomaticReconnect()。
Hub 事件監聽:
| Hub 事件 | 參數 | 處理邏輯 |
|---|---|---|
StateChanged | previousState, currentState, engineState | 更新 CurrentStateName、EngineState |
ScreenUpdated | frame (byte[]) | 更新 ScreenFrame |
LogReceived | level, message, durationMs | 新增 ConsoleLogEntry,超過 500 筆移除最舊 |
DotGraphUpdated | dot (string) | 更新 DotGraph |
VariablesUpdated | vars (Dictionary) | 清空後替換整組變數 |
控制方法呼叫對應:
| 方法 | Hub Invoke |
|---|---|
ExecuteScriptAsync(code) | "Execute" |
PauseAsync() | "Pause" |
ResumeAsync() | "Resume" |
AbortAsync() | "Abort" |
SetBreakpointAsync(line) | "SetBreakpoint" |
RemoveBreakpointAsync(line) | "RemoveBreakpoint" |
StepOverAsync() | "StepOver" |
ContinueAsync() | "Continue" |
所有控制方法在呼叫前皆檢查 _hub?.State == HubConnectionState.Connected。
3.3 MockConsoleStateService
開發與測試用的 Mock 實作,從 wwwroot/mock/{MockScenario}.json 載入預錄情境並以 Timer 按步驟重播。
public class MockConsoleStateService : IRpaConsoleStateService
{
public MockConsoleStateService(RpaWebOptions options)
// ...
}
Mock Scenario 格式:
internal record MockScenario(
string Name,
string DotGraph,
List<MockScenarioStep> Steps);
internal record MockScenarioStep(
string StateName,
int DelayMs,
string? ScreenshotBase64,
MockLogEntry? Log,
Dictionary<string, object?>? Variables);
internal record MockLogEntry(
string Message,
string Level,
int? DurationMs);
ExecuteScriptAsync()載入 scenario 後自動逐步播放PauseAsync()/ResumeAsync()控制 Timer 暫停與恢復StepOverAsync()手動前進一步後立即暫停ContinueAsync()等同ResumeAsync()ConnectAsync()為 no-op(無需實際連線)- 內部以
lock (_syncRoot)保護 log / timeline / variables 的 thread safety
3.4 RpaWebOptions
public class RpaWebOptions
{
public bool UseMockEngine { get; set; } = true;
public string MockScenario { get; set; } = "demo-scenario";
public string SignalRHubUrl { get; set; } = "/rpahub";
}
| 屬性 | 預設值 | 說明 |
|---|---|---|
UseMockEngine | true | true 時注入 MockConsoleStateService;false 時注入 SignalRConsoleStateService |
MockScenario | "demo-scenario" | Mock 情境檔名(不含 .json),從 wwwroot/mock/ 載入 |
SignalRHubUrl | "/rpahub" | SignalR Hub 端點路徑 |
3.5 操作流程時序
以「使用者按下 Execute 按鈕」為例,從 Browser 點擊到 UI 即時更新的完整路徑:
時序的三個層次:
- 同步控制路徑:
Browser → State → Hub → Worker是一次性的 RPC;InvokeAsync完成才算指令送達 Worker - 異步事件路徑:執行期間 Worker 持續
SendAsync推送狀態 / 截圖 / 日誌,State 端的 handler 更新本地狀態並觸發OnChange - UI 渲染路徑:所有訂閱
OnChange的元件(Editor、Graph、Log、Timeline、Variables)一起 re-render,達成多面板同步
4. JS Interop
4.1 MonacoInterop
封裝 monaco-interop.js,透過 IJSRuntime 呼叫 JavaScript 函式。
public sealed class MonacoInterop(IJSRuntime js)
{
Task SetBreakpointDecorationsAsync(
IJSObjectReference editorInstance, int[] lineNumbers);
Task SetErrorMarkersAsync(
string modelUri, IEnumerable<ScriptErrorMarker> errors);
Task HighlightCurrentLineAsync(
IJSObjectReference editorInstance, int lineNumber);
}
| 方法 | JS 函式 | 用途 |
|---|---|---|
SetBreakpointDecorationsAsync | monacoInterop.setBreakpointDecorations | 在指定行號顯示 breakpoint glyph |
SetErrorMarkersAsync | monacoInterop.setErrorMarkers | 設定 inline 錯誤標記(紅色波浪線) |
HighlightCurrentLineAsync | monacoInterop.highlightCurrentLine | 高亮目前執行暫停的行 |
輔助型別:
public record ScriptErrorMarker(int Line, int Column, string Message);
4.2 GraphvizInterop
封裝 graphviz-interop.js,透過 D3-graphviz 將 DOT 字串渲染為 SVG。實作 IAsyncDisposable 以釋放 JavaScript 端資源。
public sealed class GraphvizInterop(IJSRuntime js) : IAsyncDisposable
{
Task RenderDotAsync(string elementId, string dotString);
Task HighlightStatesAsync(
string elementId, string? currentState, IEnumerable<string> visitedStates);
ValueTask DisposeAsync();
}
| 方法 | JS 函式 | 用途 |
|---|---|---|
RenderDotAsync | graphvizInterop.renderDot | 將 DOT 渲染至指定 DOM 元素 |
HighlightStatesAsync | graphvizInterop.highlightStates | 標記 current state 及已走過的 visited states |
DisposeAsync | graphvizInterop.dispose | 釋放渲染資源 |
5. 資料模型
ConsoleLogEntry
public record ConsoleLogEntry(
DateTime Timestamp,
string Level,
string Message,
int? DurationMs = null);
| 欄位 | 說明 |
|---|---|
Timestamp | 日誌產生時間 |
Level | 嚴重等級:Info、Warning、Error |
Message | 人類可讀的日誌訊息 |
DurationMs | 計時操作的耗時毫秒數(選填) |
TimelineStep
public record TimelineStep(
string StateName,
TimelineStepStatus Status,
TimeSpan? Duration = null);
public enum TimelineStepStatus
{
Pending, // 尚未到達
Current, // 正在執行
Completed, // 執行完成
Failed // 執行失敗
}
| 欄位 | 說明 |
|---|---|
StateName | State Machine 狀態顯示名稱 |
Status | 步驟執行狀態 |
Duration | 已完成步驟的實際耗時(選填) |
BreakpointInfo
public record BreakpointInfo(int LineNumber, bool IsHit = false);
| 欄位 | 說明 |
|---|---|
LineNumber | 1-based 行號 |
IsHit | 引擎目前是否暫停在此中斷點 |
6. State Container Pattern
IRpaConsoleStateService 同時扮演 State Container 的角色,採用 Blazor 推薦的共享狀態模式:
┌─────────────┐ OnChange event ┌─────────────┐
│ Component A │ ◄──────────────────────► │ State │
│ (Editor) │ │ Service │
└─────────────┘ │ │
┌─────────────┐ OnChange event │ (IRpaConsole │
│ Component B │ ◄──────────────────────► │ StateService│
│ (Graph) │ │ ) │
└─────────────┘ └──────┬───────┘
┌─────────────┐ OnChange event │
│ Component C │ ◄─────────────────────────────┘
│ (Log Panel) │
└─────────────┘
運作機制:
IRpaConsoleStateService以Scoped生命週期注入(每個 Blazor circuit 一個實例)- 各元件在
OnInitialized中訂閱OnChangeevent - 當狀態變更時(例如 SignalR 收到
StateChanged),Service 呼叫NotifyChange() - 所有已訂閱的元件收到通知後呼叫
StateHasChanged()觸發 re-render
元件端典型用法:
@inject IRpaConsoleStateService Console
@implements IDisposable
@code {
protected override void OnInitialized()
{
Console.OnChange += StateHasChanged;
}
public void Dispose()
{
Console.OnChange -= StateHasChanged;
}
}
此模式確保所有面板(Editor、Graph、Log、Timeline、Variables)保持同步,而不需元件間直接耦合。
7. 使用範例
DI 註冊(Mock 模式)
builder.Services.AddRpaWeb(options =>
{
options.UseMockEngine = true;
options.MockScenario = "DemoErp";
});
DI 註冊(正式模式)
builder.Services.AddRpaWeb(options =>
{
options.UseMockEngine = false;
options.SignalRHubUrl = "/rpahub";
});
ServiceCollectionExtensions 內部邏輯
public static IServiceCollection AddRpaWeb(
this IServiceCollection services,
Action<RpaWebOptions>? configure = null)
{
var options = new RpaWebOptions();
configure?.Invoke(options);
services.AddSingleton(options);
if (options.UseMockEngine)
services.AddScoped<IRpaConsoleStateService, MockConsoleStateService>();
else
services.AddScoped<IRpaConsoleStateService, SignalRConsoleStateService>();
return services;
}
元件注入 State Service
@page "/console"
@inject IRpaConsoleStateService Console
<h3>Engine: @Console.EngineState</h3>
<p>Current State: @Console.CurrentStateName</p>
<button @onclick="RunAsync">Execute</button>
<button @onclick="() => Console.PauseAsync()">Pause</button>
<button @onclick="() => Console.ResumeAsync()">Resume</button>
<button @onclick="() => Console.AbortAsync()">Abort</button>
@code {
protected override async Task OnInitializedAsync()
{
Console.OnChange += StateHasChanged;
await Console.ConnectAsync();
}
private async Task RunAsync()
{
var script = "/* 從 Monaco Editor 取得 */";
await Console.ExecuteScriptAsync(script);
}
public void Dispose()
{
Console.OnChange -= StateHasChanged;
}
}
8. 架構決策
為什麼選 Blazor Server?
| 考量 | 說明 |
|---|---|
| 即時性 | Blazor Server 透過 SignalR 維持永久連線,天然適合接收 Engine 的即時推播 |
| 安全性 | 所有 C# 邏輯在 Server 端執行,敏感的 RPA 腳本不會暴露到 Client |
| 無 WASM 限制 | 不受 WebAssembly sandbox 限制,可直接參照 Core / Scripting 等內部模組 |
| 部署簡易 | 單一 ASP.NET Core 程序,開發者本機即可啟動 |
Mock vs Real 雙模式設計
透過 RpaWebOptions.UseMockEngine 切換注入的實作:
- 開發階段 — 使用
MockConsoleStateService重播 JSON scenario,不需實際 RPA Engine 運行 - 整合測試 — 設計不同 scenario JSON 模擬 error / pause / breakpoint hit 等邊界情境
- 正式環境 — 使用
SignalRConsoleStateService連線實際 Engine
兩個實作共用同一 IRpaConsoleStateService 介面,元件端完全不需知道底層是 mock 還是 real。
JS Interop 策略
| 元件 | Interop 類別 | 對應 JS 模組 | 原因 |
|---|---|---|---|
| Monaco Editor | MonacoInterop | monaco-interop.js | Monaco Editor 僅有 JS API,必須透過 Interop 操作 decoration / marker |
| Graphviz | GraphvizInterop | graphviz-interop.js | D3-graphviz 為 JS library,DOT → SVG 渲染只能在瀏覽器端完成 |
Interop 類別統一使用 Primary Constructor 注入 IJSRuntime,並將 JS function name 硬編碼為字串常數,確保 C# 端與 JS 端的對應關係清晰可查。GraphvizInterop 實作 IAsyncDisposable 以確保在元件銷毀時釋放 JS 端分配的資源。
9. 版本紀錄
| 版本 | 日期 | 變更 |
|---|---|---|
| 1.0 | 2026-04-04 | 初版 |