import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { register } from "../engine/registry"; import { RuntimeError, type CapabilityHandler } from "../engine/types"; const execFileAsync = promisify(execFile); const USER32 = `Add-Type @' using System; using System.Runtime.InteropServices; public static class RpaUser32 { [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y); [DllImport("user32.dll")] public static extern void mouse_event(uint flags, uint dx, uint dy, uint data, UIntPtr extra); [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder text, int count); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); } '@`; function text(args: Record, keys: string[], fallback = ""): string { for (const key of keys) { if (args[key] !== undefined && args[key] !== null && args[key] !== "") return String(args[key]); } return fallback; } function number(args: Record, keys: string[], fallback = 0): number { const value = Number(text(args, keys, String(fallback))); return Number.isFinite(value) ? value : fallback; } function point(value: unknown): [number, number] { if (Array.isArray(value)) return [Number(value[0]) || 0, Number(value[1]) || 0]; try { const parsed = JSON.parse(String(value || "[0,0]")); return point(parsed); } catch { const values = String(value || "").split(/[ ,,]+/).map(Number); return [values[0] || 0, values[1] || 0]; } } function encoded(command: string): string { return Buffer.from(command, "utf16le").toString("base64"); } export async function powershell(command: string, timeout = 30000): Promise { try { const result = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded(command)], { windowsHide: true, timeout, maxBuffer: 4 * 1024 * 1024 }); return String(result.stdout || "").trim(); } catch (error) { const detail = error instanceof Error ? error.message : String(error); throw new RuntimeError("desktop_command_failed", `桌面操作失败:${detail}`, { detail }); } } export function psString(value: unknown): string { return `'${String(value ?? "").replace(/'/g, "''")}'`; } const keyboardSend: CapabilityHandler = async (_ctx, node, args) => { const keys = text(args, ["keys", "key", "text", "value"]); if (!keys) throw new RuntimeError("validation_error", "键盘输入为空", { nodeId: node.id, componentId: node.componentId }); await powershell(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait(${psString(keys)})`); return keys; }; const mouseClick: CapabilityHandler = async (_ctx, node, args) => { const [x, y] = point(args.position ?? args.point ?? args.location ?? "[0,0]"); const button = text(args, ["side", "button"], "0").toLowerCase(); const down = button.includes("right") || button === "1" ? 0x0008 : 0x0002; const up = button.includes("right") || button === "1" ? 0x0010 : 0x0004; await powershell(`${USER32}; [RpaUser32]::SetCursorPos(${x},${y}); [RpaUser32]::mouse_event(${down},0,0,0,[UIntPtr]::Zero); [RpaUser32]::mouse_event(${up},0,0,0,[UIntPtr]::Zero)`); return { x, y }; }; const mouseMove: CapabilityHandler = async (_ctx, node, args) => { const [x, y] = point(args.position ?? args.point ?? args.location ?? "[0,0]"); await powershell(`${USER32}; [RpaUser32]::SetCursorPos(${x},${y})`); return { x, y }; }; const mouseDrag: CapabilityHandler = async (_ctx, node, args) => { const [sx, sy] = point(args.Start ?? args.start ?? "[0,0]"); const [ex, ey] = point(args.End ?? args.end ?? "[0,0]"); await powershell(`${USER32}; [RpaUser32]::SetCursorPos(${sx},${sy}); [RpaUser32]::mouse_event(0x0002,0,0,0,[UIntPtr]::Zero); Start-Sleep -Milliseconds 80; [RpaUser32]::SetCursorPos(${ex},${ey}); Start-Sleep -Milliseconds 80; [RpaUser32]::mouse_event(0x0004,0,0,0,[UIntPtr]::Zero)`); return { start: [sx, sy], end: [ex, ey] }; }; const mouseRoll: CapabilityHandler = async (_ctx, node, args) => { const amount = number(args, ["RollNum", "rollNum", "amount"], 1) * 120; await powershell(`${USER32}; [RpaUser32]::mouse_event(0x0800,0,0,${amount},[UIntPtr]::Zero)`); return amount; }; const setActiveWindow: CapabilityHandler = async (_ctx, node, args) => { const selector = text(args, ["match_title", "select_title", "title", "target_window", "window_title"]); if (!selector) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId }); const result = await powershell(`${USER32}; $p = Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle -like ${psString(`*${selector}*`)} } | Select-Object -First 1; if (-not $p) { throw '找不到窗口' }; [RpaUser32]::SetForegroundWindow($p.MainWindowHandle); $p.MainWindowTitle`); return result; }; const getActiveWindow: CapabilityHandler = async () => powershell(`${USER32}; $sb = New-Object System.Text.StringBuilder 512; [RpaUser32]::GetWindowText([RpaUser32]::GetForegroundWindow(),$sb,$sb.Capacity) | Out-Null; $sb.ToString()`); const windowExists: CapabilityHandler = async (_ctx, node, args) => { const selector = text(args, ["match_title", "select_title", "title", "target_window", "window_title"]); if (!selector) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId }); return (await powershell(`$p = Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle -like ${psString(`*${selector}*`)} } | Select-Object -First 1; [bool]$p`)).toLowerCase() === "true"; }; const closeWindow: CapabilityHandler = async (_ctx, node, args) => { const selector = text(args, ["window_obj", "title", "target_window", "window_title"]); if (!selector) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId }); return powershell(`$p = Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle -like ${psString(`*${selector}*`)} } | Select-Object -First 1; if (-not $p) { throw '找不到窗口' }; $p.CloseMainWindow() | Out-Null; $true`); }; const runApp: CapabilityHandler = async (_ctx, node, args) => { const executable = text(args, ["executeable", "executable", "path"]); if (!executable) throw new RuntimeError("validation_error", "应用路径为空", { nodeId: node.id, componentId: node.componentId }); const params = text(args, ["params", "arguments"]); const cwd = text(args, ["cwd"]); const argPart = params ? ` -ArgumentList ${psString(params)}` : ""; const result = await powershell(`$p = Start-Process -FilePath ${psString(executable)}${argPart} ${cwd ? `-WorkingDirectory ${psString(cwd)}` : ""} -PassThru; $p.Id`); return Number(result) || result; }; const clipboardGet: CapabilityHandler = async () => powershell("Get-Clipboard -Raw"); const clipboardSet: CapabilityHandler = async (_ctx, _node, args) => { const content = text(args, ["text", "value"]); await powershell(`Set-Clipboard -Value ${psString(content)}`); return content; }; const notice: CapabilityHandler = async (_ctx, _node, args) => { const title = text(args, ["title"], "提醒"); const content = text(args, ["text", "message"]); await powershell(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show(${psString(content)},${psString(title)}) | Out-Null`); return true; }; const executeCommand: CapabilityHandler = async (_ctx, node, args) => { const command = text(args, ["action", "command"]); if (!command) throw new RuntimeError("validation_error", "命令行为空", { nodeId: node.id, componentId: node.componentId }); return powershell(command, Math.max(1000, number(args, ["timeoutMs"], 120000))); }; register("System_ExecuteCommandV1", async (ctx, node, args) => { const type = String(args.command_type ?? "1"); const dos = String(args.dos_command ?? "").trim(); const bat = String(args.bat_file ?? "").trim(); const ps = String(args.ps_command ?? "").trim(); let command = ps; if (type === "2") command = dos; else if (type === "3") command = bat ? ("& '" + bat.replace(/'/g, "''") + "'") : ""; if (!command) { throw new RuntimeError("validation_error", "命令为空", { nodeId: node.id, componentId: node.componentId, recommendation: "填写 PowerShell/DOS 命令或 BAT 文件路径" }); } return powershell(command, Math.max(1000, Number(args.timeoutMs) || 120000)); }); register(["Keyboard_Send_V1", "Keyboard_Send", "Keyboard_PressNew", "Keyboard_Press"], keyboardSend); register(["Mouse_Click", "Mouse_ClickNew"], mouseClick); register(["Mouse_MoveTo", "Mouse_Move"], mouseMove); register(["Mouse_DragTo", "Mouse_Drag"], mouseDrag); register(["Mouse_RollTo", "Mouse_Scroll"], mouseRoll); register(["Window_SetActive", "Window_Activate"], setActiveWindow); register(["Window_GetActive", "Window_GetActiveWindow"], getActiveWindow); register(["Window_WindowExists", "Window_Exists"], windowExists); register(["Window_CloseWindow", "Window_Close"], closeWindow); register(["App_Run", "App_Start"], runApp); register("Clipboard_GetText", clipboardGet); register("Clipboard_SetText", clipboardSet); register("Dialog_Notice", notice); register("Dialog_MsgBox", notice); register("System_ExecuteCommand", executeCommand);