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, 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)[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, 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));