import fs from "node:fs"; import path from "node:path"; const appData = process.env.APPDATA || path.join(process.env.USERPROFILE || "", "AppData", "Roaming"); const flowsDir = path.join(appData, "rpa-studio", "flows"); const id = "flow_manual_desktop_keymouse"; const title = "manual-desktop"; if (!fs.existsSync(flowsDir)) { throw new Error(`找不到流程目录:${flowsDir}。请先启动一次 RPA Studio。`); } let y = 80; let sequence = 0; function node(componentId, values, label, outputVar, options = {}) { const item = { id: `n${String(++sequence).padStart(2, "0")}`, componentId, label, position: { x: 260, y }, values, options: { timeoutMs: 20000, logException: true, ...(outputVar ? { outputVar } : {}), ...options } }; y += 140; return item; } function dialog(text, title = "人工确认") { return node("Dialog_Notice", { title, text }, title, undefined, { timeoutMs: 600000 }); } const nodes = [ node("Trigger.Manual", { entryName: "开始桌面键鼠人工测试" }, "人工发起"), node("System_ExecuteCommand", { action: "$old=@(Get-Process notepad -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowTitle -like '*manual-desktop*' }); foreach($p in $old){ $p | Stop-Process -Force }; if($old.Count){ Start-Sleep -Milliseconds 500; 'closed:'+$old.Count } else { 'no-old' }" }, "清理旧测试窗口", "cleanupOld"), node("System_ExecuteCommand", { action: `$d=Join-Path $env:TEMP 'rpa-manual-desktop'; New-Item -ItemType Directory -Force -Path $d | Out-Null; $f=Join-Path $d '${title}.txt'; Set-Content -LiteralPath $f -Value '' -Encoding UTF8; Write-Output $f` }, "准备记事本文件", "noteFile"), node("App_Run", { executeable: "notepad.exe", params: "${noteFile}" }, "启动记事本", "notepadPid"), node("Basic_Delay", { mileSecond: 1200 }, "等待窗口", "waited1"), node("System_ExecuteCommand", { action: `$p=Get-Process notepad -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 -and $_.MainWindowTitle -like '*${title}*' } | Select-Object -First 1; if($p){ Add-Type -MemberDefinition '[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);' -Name RpaManualMax -Namespace WinRpaManualMax | Out-Null; [void][WinRpaManualMax.RpaManualMax]::ShowWindow($p.MainWindowHandle,3); 'maximized' } else { 'no-window' }` }, "最大化记事本", "maximized"), node("Window_SetActive", { match_title: title }, "自动激活记事本", "activatedTitle", { skipOnError: true }), node("Window_GetActive", {}, "读取前台窗口", "activeTitle1"), node("Window_WindowExists", { switchType: "0", get_type: "2", window_obj: title, match_type: "1", match_title: title }, "检查窗口存在", "windowExists1"), node("Basic_DebugOutput", { msg: "自动激活:${activatedTitle};前台:${activeTitle1};存在:${windowExists1}" }, "输出阶段1结果"), dialog("阶段1 / 窗口能力\n\n预期:\n1. 屏幕上出现并最大化 manual-desktop.txt 记事本;\n2. 自动激活和前台标题包含 manual-desktop;\n3. 窗口存在为 true。\n\n确认无误后点确定。", "阶段1 · 窗口检查"), dialog("阶段2 / 键盘准备\n\n请先点确定关闭本弹窗。\n关闭后 3 秒内,用鼠标点击一次记事本编辑区,但不要按键盘。", "阶段2 · 键盘准备"), node("Basic_Delay", { mileSecond: 3000 }, "给手工聚焦留时间", "waited2"), node("Keyboard_Send", { keys: "RPA-KEY-MANUAL-OK", words: "RPA-KEY-MANUAL-OK", interval: "0.01", fuctype: "1" }, "键盘输入英文", "keyboardResult"), node("Keyboard_Send", { keys: "^a^c", words: "^a^c", interval: "0.01", fuctype: "1" }, "全选并复制", "shortcutResult"), node("Clipboard_GetText", {}, "读取剪贴板", "clipboardText"), node("Basic_DebugOutput", { msg: "键盘返回:${keyboardResult};剪贴板:${clipboardText}" }, "输出键盘结果"), dialog("阶段2 / 键盘能力\n\n预期:\n1. 记事本显示 RPA-KEY-MANUAL-OK;\n2. 调试输出中的剪贴板也是 RPA-KEY-MANUAL-OK。\n\n确认后点确定。", "阶段2 · 键盘检查"), node("Mouse_MoveTo", { position: "620,420" }, "鼠标移动", "mouseMoveResult"), node("System_ExecuteCommand", { action: "Add-Type -AssemblyName System.Windows.Forms; $p=[System.Windows.Forms.Cursor]::Position; Write-Output \"$($p.X),$($p.Y)\"" }, "核对光标位置", "realCursor"), node("Mouse_Click", { position: "650,450" }, "鼠标左键点击", "mouseClickResult"), node("Mouse_DragTo", { Start: "650,450", End: "760,520" }, "鼠标拖拽", "mouseDragResult"), node("Mouse_RollTo", { RollNum: 1 }, "滚轮滚动", "mouseRollResult"), dialog("阶段3 / 鼠标能力\n\n请回忆/观察:\n1. 光标曾移动到屏幕中部;\n2. 记事本编辑区收到左键点击;\n3. 拖拽执行且没有把窗口拖丢;\n4. 滚轮动作执行。\n\n确认后点确定。", "阶段3 · 鼠标检查"), node("Element_ExistsGround", { uiSelector: `window=${title};type=Edit;index=1` }, "UIA 元素存在", "uiaExists", { skipOnError: true }), node("Element_SetValueGround", { uiSelector: `window=${title};type=Edit;index=1`, value: "UIA-MANUAL-写入-OK" }, "UIA 写入编辑框", "uiaSetResult", { skipOnError: true }), node("Element_GetValueGround", { uiSelector: `window=${title};type=Edit;index=1` }, "UIA 读取编辑框", "uiaValue", { skipOnError: true }), node("Element_ClickGround", { uiSelector: `window=${title};type=Edit;index=1` }, "UIA 点击编辑框", "uiaClickResult", { skipOnError: true }), node("Basic_DebugOutput", { msg: "UIA 存在:${uiaExists};读取:${uiaValue}" }, "输出UIA结果"), dialog("阶段4 / UIA 能力\n\n预期:\n1. 记事本内容被写入 UIA-MANUAL-写入-OK;\n2. 调试输出中 UIA 存在为 true,读取值一致;\n3. UIA 点击执行后流程继续。\n\n确认后点确定。", "阶段4 · UIA检查"), dialog("准备清理:流程会尝试清空内容并关闭记事本。点确定继续。", "清理确认"), node("Window_SetActive", { match_title: title }, "清理前激活", "activatedTitle2", { skipOnError: true }), node("Keyboard_Send", { keys: "^a{DEL}", words: "^a{DEL}", interval: "0.01", fuctype: "1" }, "清空文本", "clearResult"), node("Window_CloseWindow", { title, window_obj: title }, "关闭记事本", "closeResult", { skipOnError: true }), node("Basic_Delay", { mileSecond: 800 }, "等待退出", "waited3"), node("Window_WindowExists", { switchType: "1", get_type: "2", window_obj: title, match_type: "1", match_title: title }, "确认窗口关闭", "windowExists2"), node("Basic_DebugOutput", { msg: "关闭结果:${closeResult};关闭后窗口存在:${windowExists2}" }, "输出清理结果"), dialog("阶段5 / 清理检查\n\n预期:记事本已关闭,关闭后窗口存在为 false。\n\n完成请点确定。", "阶段5 · 清理检查") ]; const flow = { schemaVersion: 1, id, name: "桌面键鼠人工验收", description: "分阶段验证窗口、键盘、鼠标、剪贴板、UIA 与清理;每个阶段有弹窗暂停,便于人工确认。", enabled: true, trigger: { type: "manual", label: "手动运行" }, variables: [], nodes, edges: nodes.slice(1).map((target, index) => ({ id: `e${index}`, source: nodes[index].id, target: target.id })), meta: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), appVersion: "0.1.0" } }; const target = path.join(flowsDir, `${id}.json`); fs.writeFileSync(target, JSON.stringify(flow, null, 2), "utf8"); console.log(`已写入:${target}`); console.log(`节点数:${nodes.length}`);