- createEngine(options) 支持 driver 注入,新增 engine.setDriver; 缺省保持按平台自动选择的行为 - desktop/common-final/system-extended/window-extended 四文件模块加载时 捕获 defaultEngine.driver 改为惰性 getDriver():非 win32 import 不再抛错 (此前 darwin 上 import 即炸),调用时才触发 platform_unsupported - engine-driver.test 补注入/隔离/后置替换 4 例
130 lines
6.7 KiB
TypeScript
130 lines
6.7 KiB
TypeScript
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, psText } from "../driver/windows/powershell";
|
||
import { defaultEngine } from "../engine/engine";
|
||
|
||
function value(args: Record<string, unknown>, 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<string, unknown>, 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, unknown>): 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<string, unknown>;
|
||
return String(object.title ?? object.windowTitle ?? object.pid ?? object.hwnd ?? "");
|
||
}
|
||
return String(raw ?? "");
|
||
}
|
||
|
||
// 窗口矩形/状态操作统一走驱动(任务 3.3)
|
||
const getDriver = () => defaultEngine.driver;
|
||
|
||
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<string, string> = {
|
||
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 ${psText(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);
|
||
return getDriver().window.findByTitle(title);
|
||
});
|
||
|
||
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 getDriver().window.setState(title, command);
|
||
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 getDriver().window.setSize(title, width, height);
|
||
return { width, height };
|
||
});
|