Root cause of second failure (120s timeout): VB InputBox has no topmost flag and no taskbar presence, so it renders BEHIND the main window. User never sees it, flow waits until node timeout (120000ms). Fix: - Dialog_InputBox now builds a WinForms form with TopMost=true, ShowInTaskbar=true, CenterScreen, OK/Cancel buttons - MessageBox notice adds MB_TOPMOST via options param - Flow updated: 3 sequential InputBox nodes replaced by ONE Dialog_GetConfigV7 form (topmost BrowserWindow via host bridge), 3 fields filled in a single window, wait_time=10min - Flow node timeout raised to 660s to exceed the 600s PS-level wait - Fixed latent bug: Time_WeekDay node had empty outputVar, so the "星期几" variable never existed
283 lines
16 KiB
TypeScript
283 lines
16 KiB
TypeScript
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<void> {
|
||
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<string, unknown>, 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<string, unknown>, 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);
|
||
|