import { register } from "../engine/registry"; import { RuntimeError, type CapabilityHandler } from "../engine/types"; import { type ManualOperationSettings, defaultEngine } from "../engine/engine"; import { powershell, psString } from "../driver/windows/powershell"; // 人为拟真设置本体挂在引擎实例上(任务 1.3),此处委托 defaultEngine。 const manualOperation: ManualOperationSettings = defaultEngine.manualOperation; // 桌面自动化驱动(任务 3.3):user32 注入原语统一走 WindowsDriver const driver = defaultEngine.driver; function jitter(value: number, amount = 3): number { return Math.max(0, Math.round(value + (Math.random() * amount * 2 - amount))); } function manualJitter(point: [number, number], move: boolean, click: boolean): [number, number] { if (!manualOperation.enabled) return point; const amount = (click && manualOperation.randomClick) || (move && manualOperation.randomMove) ? 3 : 0; return [jitter(point[0], amount), jitter(point[1], amount)]; } async function manualDelay(): Promise { if (!manualOperation.enabled || !manualOperation.randomSleep) return; const low = Math.max(0, manualOperation.minMs); const high = Math.max(low, manualOperation.maxMs); const timeout = low + Math.random() * (high - low); if (timeout > 0) await new Promise((resolve) => setTimeout(resolve, timeout)); } function text(args: Record, 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 bool(value: unknown): boolean { return [true, 1, "1", "true", "True", "是", "开启"].includes(value as never); } function number(args: Record, 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]; } } const keyboardSend: CapabilityHandler = async (_ctx, node, args) => { // 字段: words=输入内容(键盘输入), keys/key=按键组合(按键系列), interval=输入间隔(V1毫秒/基础版秒), fuctype=输入方式(0后台接口/1人工模拟/2剪贴板粘贴), window_title=输入目标窗口(留空=当前焦点窗口) const keys = text(args, ["words", "keys", "key", "text", "value"]); if (!keys) throw new RuntimeError("validation_error", "键盘输入为空", { nodeId: node.id, componentId: node.componentId }); await manualDelay(); const rawInterval = number(args, ["interval"], 0); const intervalMs = node.componentId === "Keyboard_Send" ? Math.max(0, Math.round(rawInterval * 1000)) : Math.max(0, Math.round(rawInterval)); const mode = text(args, ["fuctype"], "0"); const isTextSend = node.componentId === "Keyboard_Send" || node.componentId === "Keyboard_Send_V1"; // 指定目标窗口时先激活再输入,内容才会进到目标窗口而不是运行时的焦点窗口 const targetTitle = text(args, ["window_title", "target_window", "window_obj", "match_title", "select_title"]); await driver.keyboard.send({ keys, intervalMs, mode: mode === "2" ? "clipboard" : mode === "1" ? "manual" : "api", textMode: isTextSend, activateTitle: targetTitle || undefined }); await manualDelay(); return keys; }; const enableManualOperation: CapabilityHandler = async (_ctx, _node, args) => { const min = number(args, ["min_time", "minTime"], 1) * 1000; const max = number(args, ["max_time", "maxTime"], 3) * 1000; manualOperation.enabled = true; manualOperation.randomMove = bool(args.random_move ?? true); manualOperation.randomClick = bool(args.random_click ?? true); manualOperation.randomSleep = bool(args.random_sleep ?? true); manualOperation.minMs = min; manualOperation.maxMs = Math.max(min, max); return { enabled: true, randomMove: manualOperation.randomMove, randomClick: manualOperation.randomClick, randomSleep: manualOperation.randomSleep, minMs: manualOperation.minMs, maxMs: manualOperation.maxMs }; }; const disableManualOperation: CapabilityHandler = async () => { manualOperation.enabled = false; return { enabled: false }; }; const mouseClick: CapabilityHandler = async (_ctx, node, args) => { const [x, y] = manualJitter(point(args.position ?? args.point ?? args.location ?? "[0,0]"), false, true); const button = text(args, ["side", "button"], "0").toLowerCase(); await driver.mouse.click({ x, y }, { button: button.includes("right") || button === "1" ? "right" : "left" }); return { x, y }; }; const mouseMove: CapabilityHandler = async (_ctx, node, args) => { const [x, y] = manualJitter(point(args.position ?? args.point ?? args.location ?? "[0,0]"), true, false); await driver.mouse.moveTo({ 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 driver.mouse.drag({ x: sx, y: sy }, { x: ex, y: ey }, { holdMs: 80, releaseDelayMs: 80 }); return { start: [sx, sy], end: [ex, ey] }; }; const mouseRoll: CapabilityHandler = async (_ctx, node, args) => { const amount = number(args, ["RollNum", "rollNum", "amount"], 1) * 120; await driver.mouse.scroll(amount); return amount; }; const setActiveWindow: CapabilityHandler = async (_ctx, node, args) => { const selector = text(args, ["window_obj", "match_title", "select_title", "title", "target_window", "window_title"]); if (!selector) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId }); return driver.window.focus(selector); }; const getActiveWindow: CapabilityHandler = async () => driver.window.foregroundTitle(); const windowExists: CapabilityHandler = async (_ctx, node, args) => { const selector = text(args, ["window_obj", "match_title", "select_title", "title", "target_window", "window_title"]); if (!selector) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId }); return driver.window.exists(selector); }; const closeWindow: CapabilityHandler = async (_ctx, node, args) => { const selector = text(args, ["window_obj", "match_title", "select_title", "title", "target_window", "window_title"]); if (!selector) throw new RuntimeError("validation_error", "窗口标题为空", { nodeId: node.id, componentId: node.componentId }); return driver.window.close(selector); }; 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"]); // MessageBox 阻塞等用户点确定,同样不能吃 powershell() 默认 30s 超时;且 MB_TOPMOST 保证弹窗不被主窗口盖住 const waitSeconds = number(args, ["wait_time", "waitTime", "timeout"], 600); const timeoutMs = Math.max(10000, Math.round(waitSeconds * 1000)); await powershell(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show(${psString(content)},${psString(title)},'OK','Information','Button1','HelpButton',1) | Out-Null`, timeoutMs); 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("Keyboard_EnableManualOperation", enableManualOperation); register("Keyboard_DisableManualOperation", disableManualOperation); 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"], "所有文件|*.*"); // ShowDialog 阻塞等用户选择,等待上限给到 30 分钟(文件挑选通常耗时更久) 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 { '' }", 1800000); }); 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 { '' }", 1800000); }); 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 { '' }", 1800000); }); 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"], ""); // 交互对话框会一直阻塞等用户输入,必须给 powershell() 传足够长的超时(默认 30s 会把还在等人的对话框进程杀掉,报 Command failed)。 // 默认 10 分钟,可用 wait_time(秒)自定义;仍超时则报「用户未及时输入」而非笼统的 Command failed。 const waitSeconds = number(args, ["wait_time", "waitTime", "timeout"], 600); const timeoutMs = Math.max(10000, Math.round(waitSeconds * 1000)); // VB InputBox 不置顶也不在任务栏显示,容易被主窗口盖住导致"看不见弹窗→干等超时"。 // 改用 WinForms 窗口:Topmost 强制置顶 + ShowInTaskbar + StartPosition 屏幕中央,保证用户一定看得见。 const script = [ "Add-Type -AssemblyName System.Windows.Forms | Out-Null", "Add-Type -AssemblyName System.Drawing | Out-Null", "$form = New-Object System.Windows.Forms.Form", "$form.Text = " + psString(title), "$form.Size = New-Object System.Drawing.Size(460, 240)", "$form.StartPosition = 'CenterScreen'", "$form.TopMost = $true", "$form.ShowInTaskbar = $true", "$form.FormBorderStyle = 'FixedDialog'", "$form.MaximizeBox = $false", "$label = New-Object System.Windows.Forms.Label", "$label.Text = " + psString(description), "$label.Location = New-Object System.Drawing.Point(14, 12)", "$label.Size = New-Object System.Drawing.Size(416, 60)", "$box = New-Object System.Windows.Forms.TextBox", "$box.Text = " + psString(defaultValue), "$box.Location = New-Object System.Drawing.Point(14, 78)", "$box.Size = New-Object System.Drawing.Size(416, 26)", "$ok = New-Object System.Windows.Forms.Button", "$ok.Text = '确定'", "$ok.DialogResult = [System.Windows.Forms.DialogResult]::OK", "$ok.Location = New-Object System.Drawing.Point(256, 150)", "$ok.Size = New-Object System.Drawing.Size(84, 30)", "$form.AcceptButton = $ok", "$cancel = New-Object System.Windows.Forms.Button", "$cancel.Text = '取消'", "$cancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel", "$cancel.Location = New-Object System.Drawing.Point(346, 150)", "$cancel.Size = New-Object System.Drawing.Size(84, 30)", "$form.Controls.AddRange(@($label, $box, $ok, $cancel))", "$form.Add_Shown({ $form.Activate() })", "$result = $form.ShowDialog()", "if ($result -eq [System.Windows.Forms.DialogResult]::OK) { $box.Text } else { '' }" ].join("; "); return powershell(script, timeoutMs).then((value) => ({ value, canceled: value === "" })).catch((error) => { if (error instanceof RuntimeError && /Command failed/.test(error.message) && !error.message.includes("RPA_ERROR")) { throw new RuntimeError("dialog_timeout", `输入对话框等待超时(${Math.round(timeoutMs / 1000)} 秒)用户未完成输入`, { nodeId: _node.id, componentId: _node.componentId, recommendation: "置顶输入框应显示在屏幕最前;若仍超时请调大节点的超时时间或 wait_time 参数" }); } throw error; }); }); register("Dialog_Notice", notice); register("Dialog_MsgBox", notice); register("System_ExecuteCommand", executeCommand);