102 lines
3.6 KiB
TypeScript
102 lines
3.6 KiB
TypeScript
import { app, type WebContents } from "electron";
|
|
import { overlayBroadcast, hideRunOverlay, showRunOverlay } from "../overlay";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import type { FlowDefinition, RunEvent } from "../../shared/types";
|
|
import { loadCatalog } from "../catalog";
|
|
import { runFlow } from "../engine/runtime";
|
|
import { BrowserSession } from "../browser/session";
|
|
|
|
const active = new Map<string, AbortController>();
|
|
let currentRunId: string | null = null;
|
|
let _getMainWindow: () => import("electron").BrowserWindow | null = () => null;
|
|
export function setMainWindowGetter(fn: () => import("electron").BrowserWindow | null) { _getMainWindow = fn; }
|
|
function getMainWindow() { return _getMainWindow(); }
|
|
|
|
function newRunId(): string {
|
|
return `run_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
}
|
|
|
|
export function startRun(flow: FlowDefinition, sender: WebContents): { runId: string } {
|
|
if (currentRunId) active.get(currentRunId)?.abort();
|
|
|
|
const runId = newRunId();
|
|
const root = path.join(app.getPath("userData"), "runs", runId);
|
|
const evidenceDir = path.join(root, "evidence");
|
|
fs.mkdirSync(evidenceDir, { recursive: true });
|
|
|
|
const controller = new AbortController();
|
|
active.set(runId, controller);
|
|
currentRunId = runId;
|
|
showRunOverlay(getMainWindow());
|
|
|
|
const catalog = loadCatalog();
|
|
const components = new Map(catalog.components.map((item) => [item.id, item]));
|
|
|
|
const nodeLabels = new Map<string, string>();
|
|
for (const node of flow.nodes) {
|
|
nodeLabels.set(node.id, node.label || node.componentId);
|
|
}
|
|
const emit = (event: Omit<RunEvent, "runId">) => {
|
|
const enriched = { ...event, runId } as RunEvent;
|
|
if (event.type === "node.started" && event.nodeId) {
|
|
enriched.nodeLabel = nodeLabels.get(event.nodeId);
|
|
}
|
|
if (!sender.isDestroyed()) sender.send("run:event", enriched);
|
|
overlayBroadcast(enriched);
|
|
};
|
|
const log: (message: string, level?: "info" | "warn" | "error", nodeId?: string) => void = (
|
|
message,
|
|
level = "info",
|
|
nodeId
|
|
) => emit({ type: "node.log", nodeId, level, message });
|
|
|
|
void (async () => {
|
|
try {
|
|
const result = await runFlow(flow, {
|
|
runId,
|
|
evidenceDir,
|
|
signal: controller.signal,
|
|
emit,
|
|
log,
|
|
componentOf: (node) => components.get(node.componentId)
|
|
});
|
|
fs.writeFileSync(path.join(root, "run.json"), JSON.stringify({ ...result, flowId: flow.id }, null, 2), "utf8");
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
emit({ type: "run.finished", status: "failed", durationMs: 0, error: { code: "unknown", reason: message } });
|
|
fs.writeFileSync(
|
|
path.join(root, "run.json"),
|
|
JSON.stringify({ runId, flowId: flow.id, status: "failed", error: { reason: message }, finishedAt: new Date().toISOString(), durationMs: 0 }, null, 2),
|
|
"utf8"
|
|
);
|
|
} finally {
|
|
active.delete(runId);
|
|
if (currentRunId === runId) currentRunId = null;
|
|
hideRunOverlay(getMainWindow());
|
|
}
|
|
})();
|
|
|
|
return { runId };
|
|
}
|
|
|
|
export function stopRun(runId?: string): boolean {
|
|
const id = runId || currentRunId;
|
|
if (!id) return false;
|
|
const controller = active.get(id);
|
|
if (!controller) return false;
|
|
controller.abort();
|
|
return true;
|
|
}
|
|
|
|
export async function closeBrowserSession(): Promise<void> {
|
|
if (globalThis.__rpaBrowserWindow && !globalThis.__rpaBrowserWindow.isDestroyed()) {
|
|
const session = await BrowserSession.create();
|
|
await session.close();
|
|
}
|
|
}
|
|
|
|
export function isRunning(): boolean {
|
|
return currentRunId !== null;
|
|
}
|