import { app, BrowserWindow, dialog, ipcMain, shell } from "electron"; import path from "node:path"; import type { AppInfo, AppSettings, FlowDefinition, FlowSaveOptions, FlowSaveResult } from "../shared/types"; import { BrowserSession, externalBrowserSession, hasBrowserSession, latestExternalSession, setBrowserElementWaitMs } from "./browser/session"; import "./capabilities"; import { catalogPath, loadCatalog } from "./catalog"; import { createDemoFlow, createExampleFlows, createFlow, deleteFlow, getFlow, listFlows, saveFlow } from "./flows"; import { capabilityCount, capabilityIds } from "./engine/registry"; import { closeBrowserSession, startRun, stopRun } from "./run/manager"; import { fixtureUrl, startFixtureServer, stopFixtureServer } from "./fixture/server"; import { loadSettings, updateSettings } from "./settings"; import { closeLocalVisionWorkers, defaultUiModelPath } from "./local-vision/ui-detect-service"; import { pickDesktopElement, listVisibleWindows, pickScreenRegion } from "./desktop-pick"; import { detectVisionLabTarget, listVisionLabSources } from "./vision-lab"; import { logMain } from "./logger"; let mainWindow: BrowserWindow | null = null; if (process.env.RPA_ENABLE_GPU !== "1") app.disableHardwareAcceleration(); process.on("uncaughtException", (error) => logMain("uncaughtException", error.stack || error.message)); process.on("unhandledRejection", (reason) => logMain("unhandledRejection", reason instanceof Error ? reason.stack || reason.message : String(reason))); function createWindow(): void { const e2e = process.env.RPA_E2E === "1"; logMain("createWindow", { packaged: app.isPackaged, appPath: app.getAppPath(), userData: app.getPath("userData") }); mainWindow = new BrowserWindow({ width: 1440, height: 900, minWidth: 1200, minHeight: 760, show: false, backgroundColor: "#E7EDF3", title: "RPA Studio", titleBarStyle: "hidden", titleBarOverlay: { color: "#FFFFFF", symbolColor: "#4E5A6B", height: 58 }, webPreferences: { preload: path.join(__dirname, "../preload/index.js"), contextIsolation: true, nodeIntegration: false, sandbox: false } }); let shown = false; const showWindow = (source: string) => { if (e2e || shown || !mainWindow || mainWindow.isDestroyed()) return; shown = true; logMain("showWindow", source); mainWindow.show(); mainWindow.focus(); }; const showFallback = setTimeout(() => showWindow("fallback-timeout"), 5000); mainWindow.on("ready-to-show", () => { clearTimeout(showFallback); showWindow("ready-to-show"); }); mainWindow.on("unresponsive", () => logMain("window-unresponsive")); mainWindow.on("responsive", () => logMain("window-responsive")); mainWindow.on("closed", () => { mainWindow = null; }); mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (/^https?:/i.test(url)) void shell.openExternal(url); return { action: "deny" }; }); mainWindow.webContents.on("did-fail-load", (_event, code, description, url) => { logMain("did-fail-load", { code, description, url }); showWindow("did-fail-load"); }); mainWindow.webContents.on("render-process-gone", (_event, details) => logMain("render-process-gone", details)); mainWindow.webContents.on("console-message", (_event, level, message, line, sourceId) => { if (level >= 2) logMain("renderer-console", { level, message, line, sourceId }); }); const devUrl = process.env.ELECTRON_RENDERER_URL; if (devUrl) { void mainWindow.loadURL(devUrl); } else { void mainWindow.loadFile(path.join(__dirname, "../renderer/index.html")); } } function registerIpc(): void { ipcMain.handle("app:info", (): AppInfo => ({ version: app.getVersion(), electron: process.versions.electron, chrome: process.versions.chrome, node: process.versions.node, userData: app.getPath("userData"), catalogPath: catalogPath(), packaged: app.isPackaged, fixtureUrl: fixtureUrl() })); ipcMain.handle("catalog:get", () => loadCatalog()); ipcMain.handle("run:start", (event, flow: FlowDefinition) => startRun(flow, event.sender)); ipcMain.handle("run:stop", (_event, runId?: string) => stopRun(runId)); ipcMain.handle("run:capabilities", () => ({ count: capabilityCount(), ids: capabilityIds() })); ipcMain.handle("settings:get", () => loadSettings()); ipcMain.handle("settings:update", (_event, patch: Partial = {}) => { const settings = updateSettings(patch); setBrowserElementWaitMs(settings.browserElementWaitMs); return settings; }); type BrowserPickOptions = { url?: string; browser?: import("./browser/external").ExternalBrowserOptions; }; ipcMain.handle("browser:pick-element", async (_event, options: BrowserPickOptions = {}) => { const url = options.url?.trim(); const browserOptions = options.browser; let session; if (browserOptions) { session = await externalBrowserSession( browserOptions.mode === "launch" && url ? { ...browserOptions, initialUrl: url } : browserOptions ); } else { session = latestExternalSession(); } if (!session) { if (!hasBrowserSession() && !url) { throw new Error("请先配置或运行「打开网页」,再拾取元素"); } session = await BrowserSession.create(); } const currentUrl = await session.currentUrl(); if (url && currentUrl !== url) { await session.navigate(url); } return session.pickElement(); }); ipcMain.handle("desktop:pick-element", () => pickDesktopElement()); ipcMain.handle("desktop:pick-window-list", () => listVisibleWindows()); ipcMain.handle("desktop:pick-region", () => pickScreenRegion()); ipcMain.handle("vision-lab:sources", () => listVisionLabSources()); ipcMain.handle("vision-lab:detect", (_event, request: Parameters[0]) => detectVisionLabTarget(request)); ipcMain.handle("vision-lab:default-model", () => defaultUiModelPath()); ipcMain.handle("flows:list", () => listFlows()); ipcMain.handle("flows:create", (_event, name?: string) => { const flow = createFlow(name); return saveFlow(flow); }); ipcMain.handle("flows:create-demo", () => { const url = fixtureUrl(); return saveFlow(url ? createDemoFlow(url) : createFlow("未命名任务")); }); ipcMain.handle("flows:create-examples", () => { const url = fixtureUrl(); if (!url) throw new Error("本地演示站未启动"); for (const flow of createExampleFlows(url)) saveFlow(flow); return listFlows(); }); ipcMain.handle("flows:get", (_event, id: string) => { return getFlow(id); }); ipcMain.handle("flows:save", (_event, flow: FlowDefinition, opts?: FlowSaveOptions): FlowSaveResult => { if (!opts?.force && opts?.expectedUpdatedAt) { let disk: FlowDefinition | null = null; try { disk = getFlow(flow.id); } catch { disk = null; // 磁盘文件不存在或损坏时视为全新保存 } if (disk && disk.meta?.updatedAt && disk.meta.updatedAt !== opts.expectedUpdatedAt) { // 内容一致(仅 updatedAt 差异,如并发/重复保存)不视为冲突 const normalize = (item: FlowDefinition) => JSON.stringify({ ...item, meta: undefined }); if (normalize(disk) !== normalize(flow)) { return { conflict: true, diskFlow: disk }; } } } return { conflict: false, flow: saveFlow(flow) }; }); ipcMain.handle("flows:delete", (_event, id: string) => { deleteFlow(id); return true; }); ipcMain.handle("dialog:open", async (_event, options: { title?: string; filters?: Electron.FileFilter[]; directories?: boolean }) => { if (!mainWindow) return null; const result = await dialog.showOpenDialog(mainWindow, { title: options?.title || "选择文件", properties: options?.directories ? ["openDirectory"] : ["openFile"], filters: options?.filters }); return result.canceled ? null : result.filePaths[0] || null; }); ipcMain.handle("dialog:save", async (_event, options: { title?: string; defaultPath?: string; filters?: Electron.FileFilter[] }) => { if (!mainWindow) return null; const result = await dialog.showSaveDialog(mainWindow, { title: options?.title || "保存文件", defaultPath: options?.defaultPath, filters: options?.filters }); return result.canceled ? null : result.filePath || null; }); } // 打包冒烟/自动化测试需要与用户日常实例隔离;正常启动仍然保持单实例。 const gotLock = process.env.RPA_SKIP_SINGLE_INSTANCE === "1" ? true : app.requestSingleInstanceLock(); if (!gotLock) { app.quit(); } else { app.on("second-instance", () => { if (!mainWindow) return; if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); }); app.whenReady().then(async () => { logMain("app-ready", { electron: process.versions.electron, chrome: process.versions.chrome, node: process.versions.node }); setBrowserElementWaitMs(loadSettings().browserElementWaitMs); try { await startFixtureServer(Number(process.env.RPA_FIXTURE_PORT || 17863)); } catch (error) { console.warn("local fixture server unavailable", error); logMain("fixture-server-failed", error instanceof Error ? error.stack || error.message : String(error)); } registerIpc(); createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); }); app.on("before-quit", () => { void closeBrowserSession(); void stopFixtureServer(); closeLocalVisionWorkers(); }); }