- 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 例
93 lines
9.1 KiB
TypeScript
93 lines
9.1 KiB
TypeScript
import { createHmac } from "node:crypto";
|
||
import { promises as fs } from "node:fs";
|
||
import path from "node:path";
|
||
import { PDFDocument, StandardFonts, degrees, rgb } from "pdf-lib";
|
||
import { getHandler, register } from "../engine/registry";
|
||
import { RuntimeError, type CapabilityHandler } from "../engine/types";
|
||
import { powershell, psString } from "../driver/windows/powershell";
|
||
import { defaultEngine } from "../engine/engine";
|
||
|
||
// 窗口/屏幕查询统一走驱动(任务 3.3)
|
||
const getDriver = () => defaultEngine.driver;
|
||
|
||
function text(args: Record<string, unknown>, keys: string[], fallback = ""): string {
|
||
for (const key of keys) { const value = args[key]; if (value !== undefined && value !== null && value !== "") return String(value); }
|
||
return fallback;
|
||
}
|
||
function delegate(baseId: string): CapabilityHandler {
|
||
return (ctx, node, args, component) => { const base = getHandler(baseId); if (!base) throw new RuntimeError("capability_not_implemented", `基础能力不可用:${baseId}`, { nodeId: node.id, componentId: node.componentId }); return base(ctx, node, args, component); };
|
||
}
|
||
|
||
register("Window_ActiveWindow", delegate("Window_SetActive"));
|
||
register("Window_CloseWindow_v1", delegate("Window_CloseWindow"));
|
||
register("Window_GetRect", delegate("Window_GetWindowRect"));
|
||
register("Window_MaximumSize", (ctx, node, args, component) => delegate("Window_SetWindowState")(ctx, node, { ...args, WindowType: "maximize" }, component));
|
||
register(["Keyboard_DriveSend", "Keyboard_DriveSend_V1", "Keyboard_DriveSend_V2"], delegate("Keyboard_Send_V1"));
|
||
|
||
register("Window_GetScreenSize", async () => getDriver().screen.size());
|
||
register("Window_WindowList", async (_ctx, _node, args) => {
|
||
const title = text(args, ["match_title", "select_title"]); const regex = String(args.is_regex ?? "0") === "1";
|
||
return getDriver().window.list({ title, regex });
|
||
});
|
||
register("Window_SetWindowShow", async (_ctx, node, args) => {
|
||
const title = text(args, ["窗口对象", "window_obj", "title"]); if (!title || title === "窗口对象") throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId });
|
||
const show = !text(args, ["WindowType"], "show").toLowerCase().includes("hide");
|
||
await getDriver().window.setState(title, show ? 5 : 0);
|
||
return show;
|
||
});
|
||
register("Window_WaitWindow", async (_ctx, node, args) => {
|
||
const title = text(args, ["match_title", "select_title", "window_obj", "target_window"]); if (!title) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId });
|
||
const expected = text(args, ["switchType"], "0") !== "1"; const deadline = Date.now() + Math.max(0, Number(args.second ?? 5)) * 1000;
|
||
do { const exists = await getDriver().window.exists(title); if (exists === expected) return true; await new Promise((resolve) => setTimeout(resolve, 200)); } while (Date.now() <= deadline);
|
||
return false;
|
||
});
|
||
|
||
const setClipboardImage: CapabilityHandler = async (_ctx, node, args) => {
|
||
const file = path.resolve(text(args, ["path"])); await fs.access(file).catch(() => { throw new RuntimeError("validation_error", `图片不存在:${file}`, { nodeId: node.id, componentId: node.componentId }); });
|
||
await powershell(`Add-Type -AssemblyName System.Windows.Forms;Add-Type -AssemblyName System.Drawing;$image=[System.Drawing.Image]::FromFile(${psString(file)});[System.Windows.Forms.Clipboard]::SetImage($image);$image.Dispose()`); return file;
|
||
};
|
||
register(["Clipboard_SetImage", "Clipboard_SetImageV1"], setClipboardImage);
|
||
const saveClipboardImage: CapabilityHandler = async (_ctx, node, args) => {
|
||
const folder = path.resolve(text(args, ["path"])); const name = text(args, ["filename"], "clipboard.png"); const target = path.join(folder, path.extname(name) ? name : `${name}.png`); await fs.mkdir(folder, { recursive: true });
|
||
await powershell(`Add-Type -AssemblyName System.Windows.Forms;Add-Type -AssemblyName System.Drawing;if(-not [System.Windows.Forms.Clipboard]::ContainsImage()){throw '剪贴板中没有图片'};$image=[System.Windows.Forms.Clipboard]::GetImage();$image.Save(${psString(target)},[System.Drawing.Imaging.ImageFormat]::Png);$image.Dispose()`).catch((error) => { throw new RuntimeError("desktop_command_failed", (error as Error).message, { nodeId: node.id, componentId: node.componentId }); }); return target;
|
||
};
|
||
register(["Clipboard_SaveImage", "Clipboard_SaveImageV1"], saveClipboardImage);
|
||
|
||
register("System_PlaySound", async (_ctx, node, args) => {
|
||
const audio = text(args, ["audio"]); if (audio) { const file = path.resolve(audio); await fs.access(file); await powershell(`$player=New-Object System.Media.SoundPlayer ${psString(file)};$player.PlaySync()`); return file; }
|
||
await powershell("[System.Media.SystemSounds]::Beep.Play()"); return true;
|
||
});
|
||
register("CustomConfig_GetConfig", (_ctx, _node, args) => {
|
||
let configs: unknown = args.jsonlist; if (typeof configs === "string") { try { configs = JSON.parse(configs); } catch { configs = []; } }
|
||
const name = text(args, ["configName"]); if (Array.isArray(configs)) { const found = configs.find((item) => item && typeof item === "object" && [String((item as any).name ?? ""), String((item as any).configName ?? ""), String((item as any).key ?? "")].includes(name)); return found && typeof found === "object" && "value" in found ? (found as any).value : found ?? null; }
|
||
return configs && typeof configs === "object" ? (configs as Record<string, unknown>)[name] : null;
|
||
});
|
||
register("Basic_BusinessOrderCount", (ctx, _node, args) => {
|
||
const key = "__businessOrderCount"; const start = Number(args.start_count ?? 0); const step = Number(args.count ?? 1); const current = Number(ctx.vars[key] ?? start); const next = current + step; ctx.vars[key] = next; return next;
|
||
});
|
||
|
||
register("PDF_ConcatPdfV1", delegate("PDF_ConcatPdf"));
|
||
register(["PDF_SetPdfWaterMark", "PDF_SetPdfWaterMarkV1"], async (_ctx, node, args) => {
|
||
const input = path.resolve(text(args, ["fileInPath"])); const output = path.join(path.dirname(input), text(args, ["newPdfName"], "PDF加水印.pdf")); const content = text(args, ["markTextInfo"], "Watermark");
|
||
if (!/^[\x20-\x7E]*$/.test(content)) throw new RuntimeError("validation_error", "当前文本水印仅支持 ASCII;中文水印需嵌入字体", { nodeId: node.id, componentId: node.componentId });
|
||
const document = await PDFDocument.load(await fs.readFile(input), { ignoreEncryption: true }); const font = await document.embedFont(StandardFonts.Helvetica);
|
||
for (const page of document.getPages()) { const { width, height } = page.getSize(); page.drawText(content, { x: width * 0.2, y: height * 0.5, size: 32, font, color: rgb(0.65, 0.65, 0.65), opacity: 0.35, rotate: degrees(-30) }); }
|
||
await fs.writeFile(output, await document.save()); return output;
|
||
});
|
||
|
||
async function webhook(kind: "dingtalk" | "feishu" | "wecom", args: Record<string, unknown>, nodeId: string, componentId: string) {
|
||
let url = text(args, ["webhook"]); if (!/^https?:\/\//i.test(url)) throw new RuntimeError("validation_error", "Webhook 地址无效", { nodeId, componentId });
|
||
const secret = text(args, ["key"]); if (kind === "dingtalk" && secret) { const timestamp = Date.now(); const sign = encodeURIComponent(createHmac("sha256", secret).update(`${timestamp}\n${secret}`).digest("base64")); url += `${url.includes("?") ? "&" : "?"}timestamp=${timestamp}&sign=${sign}`; }
|
||
const custom = text(args, ["content_custom"]); const messageType = text(args, ["message_type"], "0"); const plain = text(args, ["content_text", "test_message"]); const markdown = text(args, ["content_markdown"]);
|
||
let body: unknown;
|
||
if (custom) { try { body = JSON.parse(custom); } catch { throw new RuntimeError("validation_error", "自定义消息 JSON 无效", { nodeId, componentId }); } }
|
||
else if (kind === "feishu") body = messageType === "1" ? { msg_type: "interactive", card: { elements: [{ tag: "markdown", content: markdown || plain }] } } : { msg_type: "text", content: { text: plain } };
|
||
else if (kind === "dingtalk") body = messageType === "1" ? { msgtype: "markdown", markdown: { title: "RPA通知", text: markdown || plain } } : { msgtype: "text", text: { content: plain }, at: { atMobiles: text(args, ["mentioned_mobile_list"]).split(/[,,]/).filter(Boolean) } };
|
||
else body = messageType === "1" ? { msgtype: "markdown", markdown: { content: markdown || plain } } : { msgtype: "text", text: { content: plain, mentioned_mobile_list: text(args, ["mentioned_mobile_list"]).split(/[,,]/).filter(Boolean) } };
|
||
const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const responseText = await response.text(); if (!response.ok) throw new RuntimeError("http_error", `Webhook 返回 ${response.status}: ${responseText}`, { nodeId, componentId });
|
||
try { return JSON.parse(responseText); } catch { return responseText; }
|
||
}
|
||
register("MessageNotification_DingTalk_Message", (_ctx, node, args) => webhook("dingtalk", args, node.id, node.componentId));
|
||
register("MessageNotification_Feishu_Message", (_ctx, node, args) => webhook("feishu", args, node.id, node.componentId));
|
||
register("MessageNotification_WXWork_Message", (_ctx, node, args) => webhook("wecom", args, node.id, node.componentId));
|