217 lines
12 KiB
TypeScript
217 lines
12 KiB
TypeScript
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<string, unknown>, 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<string, unknown>, 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<string> {
|
||
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(["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_V1", async (_ctx, node, args) => {
|
||
const msg = text(args, ["text", "message"]);
|
||
const title = text(args, ["title"], "通知");
|
||
const timeout = Math.max(1, number(args, ["timeout", "time_out"], 10));
|
||
return powershell("(New-Object -ComObject WScript.Shell).Popup(" + psString(msg) + ", " + timeout + ", " + psString(title) + ", 0) | Out-Null; $true");
|
||
});
|
||
|
||
register("Dialog_Notice_V2", async (ctx, node, args) => {
|
||
const msg = text(args, ["text", "message"]);
|
||
const title = text(args, ["title"], "通知");
|
||
const timeout = Math.max(1, number(args, ["time_out", "timeout"], 10));
|
||
return powershell("(New-Object -ComObject WScript.Shell).Popup(" + psString(msg) + ", " + timeout + ", " + psString(title) + ", 0) | Out-Null; $true");
|
||
});
|
||
|
||
register("Dialog_CsMsgBox", async (_ctx, _node, args) => {
|
||
const msg = text(args, ["text", "message"]);
|
||
const title = text(args, ["title"], "提示");
|
||
const buttons = Number(args.msg_button ?? 0);
|
||
const autoClose = Number(args.autoClose ?? 0);
|
||
const time = Math.max(1, Number(args.time ?? 10));
|
||
const popup = "(New-Object -ComObject WScript.Shell).Popup(" + psString(msg) + ", " + (autoClose ? time : -1) + ", " + psString(title) + ", " + buttons + ")";
|
||
const choice = await powershell(popup + " | Out-Null; if (" + (autoClose ? "$true" : "$false") + ") {}");
|
||
return powershell(popup);
|
||
});
|
||
|
||
register("Dialog_OpenFile", async (_ctx, _node, args) => {
|
||
const title = text(args, ["title"], "选择文件");
|
||
const wildcard = text(args, ["wildcard"], "所有文件|*.*");
|
||
return powershell("Add-Type -AssemblyName System.Windows.Forms | Out-Null; $dlg = New-Object System.Windows.Forms.OpenFileDialog; $dlg.Filter = " + psString(wildcard) + "; $dlg.Title = " + psString(title) + "; if ($dlg.ShowDialog() -eq 'OK') { $dlg.FileName } else { '' }");
|
||
});
|
||
|
||
register("Dialog_OpenFiles", async (_ctx, _node, args) => {
|
||
const title = text(args, ["title"], "选择文件");
|
||
const wildcard = text(args, ["wildcard"], "所有文件|*.*");
|
||
return powershell("Add-Type -AssemblyName System.Windows.Forms | Out-Null; $dlg = New-Object System.Windows.Forms.OpenFileDialog; $dlg.Filter = " + psString(wildcard) + "; $dlg.Title = " + psString(title) + "; $dlg.Multiselect = $true; if ($dlg.ShowDialog() -eq 'OK') { $dlg.FileNames -join ';' } else { '' }");
|
||
});
|
||
|
||
register("Dialog_SaveFile", async (_ctx, _node, args) => {
|
||
const title = text(args, ["title"], "保存文件");
|
||
const wildcard = text(args, ["wildcard"], "所有文件|*.*");
|
||
return powershell("Add-Type -AssemblyName System.Windows.Forms | Out-Null; $dlg = New-Object System.Windows.Forms.SaveFileDialog; $dlg.Filter = " + psString(wildcard) + "; $dlg.Title = " + psString(title) + "; if ($dlg.ShowDialog() -eq 'OK') { $dlg.FileName } else { '' }");
|
||
});
|
||
|
||
register(["Dialog_InputBox", "Dialog_CsInputBox"], async (_ctx, _node, args) => {
|
||
const title = text(args, ["title", "input_title"], "请输入");
|
||
const description = text(args, ["description", "txt_content"], "");
|
||
const defaultValue = text(args, ["text"], "");
|
||
return powershell("Add-Type -AssemblyName Microsoft.VisualBasic | Out-Null; [Microsoft.VisualBasic.Interaction]::InputBox(" + psString(description) + ", " + psString(title) + ", " + psString(defaultValue) + ")");
|
||
});
|
||
|
||
register("Dialog_Notice", notice);
|
||
register("Dialog_MsgBox", notice);
|
||
register("System_ExecuteCommand", executeCommand);
|
||
|