Files
PRA-only-demo/packages/rpa-core/src/capabilities/system-extended.ts
T
yu 1da00827ac feat(engine): createEngine 注入参数 + 消除模块级 driver 捕获(迁移阶段1-A1)
- 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 例
2026-11-06 10:20:29 +08:00

130 lines
6.7 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 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 };
});