PRA-only-demo/src/main/capabilities/desktop.ts
liuwenxin 5f9e28715c fix(应用与系统)!: 修复组件执行与参数校验
修正 PID 空结果、目标类型判断与转换、毫秒等待。
让关闭程序和命令行按所选模式显示并校验参数。
统一 PowerShell 输出编码,修正 BAT 路径引号处理。
明确屏保和屏幕唤醒的实际能力,补充缺陷回归测试。

验证:类型检查、构建与组件回归检查通过。

BREAKING CHANGE: 判断数据类型改为返回布尔值。
需要类型名称的旧流程请改用 Basic_Type。
屏保 V1 移除未实现参数,旧自定义密码节点需重新添加。
2026-11-02 09:05:48 +08:00

219 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 {
// Windows PowerShell 的默认输出编码依机器代码页变化;Node 按 UTF-8 解码管道。
const utf8 = "$ProgressPreference='SilentlyContinue'; [Console]::OutputEncoding=[System.Text.UTF8Encoding]::new($false); $OutputEncoding=[Console]::OutputEncoding; ";
const result = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded(utf8 + 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);