import os from "node:os"; import path from "node:path"; import { register } from "../engine/registry"; import { RuntimeError, type CapabilityHandler } from "../engine/types"; import { powershell, psString } from "./desktop"; function value(args: Record, keys: string[], fallback = ""): string { for (const key of keys) { const current = args[key]; if (current !== undefined && current !== null && current !== "") return String(current); } return fallback; } function required(args: Record, keys: string[], label: string, nodeId: string, componentId: string): string { const result = value(args, keys).trim(); if (!result) throw new RuntimeError("validation_error", `${label}为空`, { nodeId, componentId, recommendation: `请配置${label}` }); return result; } function processPattern(raw: string): string { return path.basename(raw).replace(/\.exe$/i, "").replace(/\*/g, "*") || "*"; } function selector(args: Record): string { const raw = args.window_obj ?? args["窗口对象"] ?? args.match_title ?? args.select_title ?? args.target_window ?? args.title; if (typeof raw === "object" && raw !== null) { const object = raw as Record; return String(object.title ?? object.windowTitle ?? object.pid ?? object.hwnd ?? ""); } return String(raw ?? ""); } const WINDOW_API = `Add-Type @' using System; using System.Runtime.InteropServices; public static class RpaWindowApi { [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; } [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int command); [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr after, int x, int y, int width, int height, uint flags); } '@`; function findWindowScript(title: string): string { return `$p = Get-Process | Where-Object { $_.MainWindowHandle -ne 0 -and $_.MainWindowTitle -like ${psString(`*${title}*`)} } | Select-Object -First 1; if (-not $p) { throw '找不到窗口:${title.replace(/'/g, "''")}' }`; } register("System_GetEnv", (_ctx, node, args) => { const name = required(args, ["name"], "环境变量名称", node.id, node.componentId); return process.env[name] ?? ""; }); register("System_SetEnv", (_ctx, node, args) => { const name = required(args, ["name"], "环境变量名称", node.id, node.componentId); const content = value(args, ["value"]); process.env[name] = content; return content; }); register("System_GetTempPath", () => os.tmpdir()); register("System_GetHomePath", () => os.homedir()); register("System_GetSystemPath", (_ctx, _node, args) => { const name = value(args, ["path_name"], "desktop").toLowerCase(); const home = os.homedir(); const paths: Record = { desktop: path.join(home, "Desktop"), documents: path.join(home, "Documents"), document: path.join(home, "Documents"), downloads: path.join(home, "Downloads"), download: path.join(home, "Downloads"), pictures: path.join(home, "Pictures"), music: path.join(home, "Music"), videos: path.join(home, "Videos"), home, temp: os.tmpdir(), system: process.env.SystemRoot || "C:\\Windows", appdata: process.env.APPDATA || path.join(home, "AppData", "Roaming") }; return paths[name] || value(args, ["path"]) || path.join(home, name); }); const getPids: CapabilityHandler = async (_ctx, node, args) => { const pattern = processPattern(required(args, ["pattern", "p_name", "name"], "进程名称", node.id, node.componentId)); const output = await powershell(`Get-Process | Where-Object { $_.ProcessName -like ${psString(pattern)} } | Select-Object -ExpandProperty Id`); return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) .map(Number).filter((pid) => Number.isSafeInteger(pid) && pid > 0); }; register("App_GetPids", getPids); register("App_GetPid", async (ctx, node, args, component) => (await getPids(ctx, node, args, component) as number[])[0] ?? null); register(["App_Kill", "App_KillV1"], async (_ctx, node, args) => { const mode = node.componentId === "App_Kill" ? "id" : value(args, ["close_type"], "id"); if (mode === "id") { const rawPid = args.pid; const pid = typeof rawPid === "number" || typeof rawPid === "string" ? Number(rawPid) : NaN; if (!Number.isSafeInteger(pid) || pid <= 0) { throw new RuntimeError("validation_error", "进程 PID 必须是正整数", { nodeId: node.id, componentId: node.componentId }); } if (pid === process.pid) throw new RuntimeError("validation_error", "拒绝关闭 RPA Studio 自身进程", { nodeId: node.id, componentId: node.componentId }); await powershell(`Stop-Process -Id ${pid} -Force -ErrorAction Stop`); return pid; } if (mode !== "name") throw new RuntimeError("validation_error", "请选择按 PID 或按进程名称关闭", { nodeId: node.id, componentId: node.componentId }); const pattern = processPattern(required(args, ["p_name", "pattern"], "进程名称", node.id, node.componentId)); if (pattern === "*") throw new RuntimeError("validation_error", "关闭应用程序必须指定具体的进程名称", { nodeId: node.id, componentId: node.componentId }); const allSessions = [true, 1, "1", "true", "True"].includes(args.kill_all as never); const session = allSessions ? "" : " -and $_.SessionId -eq [System.Diagnostics.Process]::GetCurrentProcess().SessionId"; await powershell(`Get-Process | Where-Object { $_.ProcessName -like ${psString(pattern)} -and $_.Id -ne ${process.pid}${session} } | Stop-Process -Force -ErrorAction Stop`); return pattern; }); register("Clipboard_GetTextV1", () => powershell("Get-Clipboard -Raw")); register("Clipboard_SetTextV1", async (_ctx, _node, args) => { const content = value(args, ["text", "value"]); await powershell(`Set-Clipboard -Value ${psString(content)}`); return content; }); register("Clipboard_ClearClipboard", async () => { await powershell("Set-Clipboard -Value $null"); return true; }); register("Window_GetWindowRect", async (_ctx, node, args) => { const title = required({ title: selector(args) }, ["title"], "窗口标题", node.id, node.componentId); const output = await powershell(`${WINDOW_API}; ${findWindowScript(title)}; $r = New-Object RpaWindowApi+RECT; [RpaWindowApi]::GetWindowRect($p.MainWindowHandle,[ref]$r) | Out-Null; @{x=$r.Left;y=$r.Top;width=$r.Right-$r.Left;height=$r.Bottom-$r.Top;title=$p.MainWindowTitle;pid=$p.Id} | ConvertTo-Json -Compress`); return JSON.parse(output); }); register(["Window_SetWindowState", "Window_SetWindowState_v1"], async (_ctx, node, args) => { const title = required({ title: selector(args) }, ["title"], "窗口标题", node.id, node.componentId); const state = value(args, ["WindowType", "switchType"], "maximize").toLowerCase(); const command = state.includes("min") ? 6 : state.includes("max") ? 3 : 9; await powershell(`${WINDOW_API}; ${findWindowScript(title)}; [RpaWindowApi]::ShowWindow($p.MainWindowHandle,${command}) | Out-Null`); return state; }); register("Window_SizeWindow_v1", async (_ctx, node, args) => { const title = required({ title: selector(args) }, ["title"], "窗口标题", node.id, node.componentId); let size: unknown = args.window_size; if (typeof size === "string") { const source = size; try { size = JSON.parse(source); } catch { size = source.split(/[,,xX ]+/).map(Number); } } const [width, height] = Array.isArray(size) ? size.map(Number) : [0, 0]; if (!(width > 0 && height > 0)) throw new RuntimeError("validation_error", "窗口尺寸无效", { nodeId: node.id, componentId: node.componentId }); await powershell(`${WINDOW_API}; ${findWindowScript(title)}; $r = New-Object RpaWindowApi+RECT; [RpaWindowApi]::GetWindowRect($p.MainWindowHandle,[ref]$r) | Out-Null; [RpaWindowApi]::SetWindowPos($p.MainWindowHandle,[IntPtr]::Zero,$r.Left,$r.Top,${width},${height},4) | Out-Null`); return { width, height }; });