fix: 窗口组件条件字段渲染+选窗口按钮; imageSelector加文件浏览与屏幕截取; prompt()改confirm(); 后端窗口selector统一读取所有标题字段; 新增桌面键鼠人工验收流程与E2E脚本
This commit is contained in:
1 parent
68e16282de
commit
b5d6bef369
9 files changed
+373
-17
No files matched your search
@@ -239,7 +239,7 @@ function Find-RpaWindow([string]$winName, [string]$winClass, [string]$processNam
|
||||
$titleSb = New-Object System.Text.StringBuilder 512
|
||||
[void][Win32Rpa.RpaUser32Click]::GetWindowTextW($h, $titleSb, 512)
|
||||
$title = $titleSb.ToString()
|
||||
if ($winName -and $title -ne $winName) { return $true }
|
||||
if ($winName -and $title -notlike "*$winName*") { return $true }
|
||||
$classSb = New-Object System.Text.StringBuilder 256
|
||||
[void][Win32Rpa.RpaUser32Click]::GetClassNameW($h, $classSb, 256)
|
||||
$className = $classSb.ToString()
|
||||
@@ -257,7 +257,7 @@ function Find-RpaWindow([string]$winName, [string]$winClass, [string]$processNam
|
||||
if ($script:hitHwnd -ne [IntPtr]::Zero) {
|
||||
try { $win = [System.Windows.Automation.AutomationElement]::FromHandle($script:hitHwnd); if ($win) { return $win } } catch {}
|
||||
}
|
||||
$missing = @($winName, $winClass, $processName) | Where-Object { $_ } -join ","
|
||||
$missing = (@($winName, $winClass, $processName) | Where-Object { $_ }) -join ","
|
||||
throw "window_not_found:$missing"
|
||||
}
|
||||
function Restore-RpaWindow($win) {
|
||||
|
||||
@@ -153,22 +153,42 @@ const mouseRoll: CapabilityHandler = async (_ctx, node, args) => {
|
||||
};
|
||||
|
||||
const setActiveWindow: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const selector = text(args, ["match_title", "select_title", "title", "target_window", "window_title"]);
|
||||
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 });
|
||||
const result = await powershell(`${USER32}; $p = Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle -like ${psString(`*${selector}*`)} } | Select-Object -First 1; if (-not $p) { throw '找不到窗口' }; [RpaUser32]::SetForegroundWindow($p.MainWindowHandle); $p.MainWindowTitle`);
|
||||
const result = await powershell(`${USER32}
|
||||
$p = Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle -like ${psString(`*${selector}*`)} } | Select-Object -First 1
|
||||
if (-not $p) { throw '找不到窗口' }
|
||||
Add-Type -MemberDefinition '[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd); [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr pid); [DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool attach); [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();' -Name RpaFocus2 -Namespace WinRpaFocus2 | Out-Null
|
||||
$h = $p.MainWindowHandle
|
||||
if ([WinRpaFocus2.RpaFocus2]::IsIconic($h)) { [void][WinRpaFocus2.RpaFocus2]::ShowWindow($h, 9) }
|
||||
if ([WinRpaFocus2.RpaFocus2]::GetForegroundWindow() -ne $h) {
|
||||
[void][WinRpaFocus2.RpaFocus2]::keybd_event(0x12, 0, 0, [UIntPtr]::Zero)
|
||||
[void][WinRpaFocus2.RpaFocus2]::SetForegroundWindow($h)
|
||||
[void][WinRpaFocus2.RpaFocus2]::keybd_event(0x12, 0, 2, [UIntPtr]::Zero)
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
if ([WinRpaFocus2.RpaFocus2]::GetForegroundWindow() -ne $h) {
|
||||
$fgThread = [WinRpaFocus2.RpaFocus2]::GetWindowThreadProcessId([WinRpaFocus2.RpaFocus2]::GetForegroundWindow(), [IntPtr]::Zero)
|
||||
$thisThread = [WinRpaFocus2.RpaFocus2]::GetCurrentThreadId()
|
||||
$attached = [WinRpaFocus2.RpaFocus2]::AttachThreadInput($thisThread, $fgThread, $true)
|
||||
try { [void][WinRpaFocus2.RpaFocus2]::SetForegroundWindow($h) } finally { if ($attached) { [void][WinRpaFocus2.RpaFocus2]::AttachThreadInput($thisThread, $fgThread, $false) } }
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
if ([WinRpaFocus2.RpaFocus2]::GetForegroundWindow() -ne $h) { throw '前台切换失败' }
|
||||
$p.MainWindowTitle`);
|
||||
return result;
|
||||
};
|
||||
|
||||
const getActiveWindow: CapabilityHandler = async () => powershell(`${USER32}; $sb = New-Object System.Text.StringBuilder 512; [RpaUser32]::GetWindowText([RpaUser32]::GetForegroundWindow(),$sb,$sb.Capacity) | Out-Null; $sb.ToString()`);
|
||||
|
||||
const windowExists: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const selector = text(args, ["match_title", "select_title", "title", "target_window", "window_title"]);
|
||||
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 (await powershell(`$p = Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle -like ${psString(`*${selector}*`)} } | Select-Object -First 1; [bool]$p`)).toLowerCase() === "true";
|
||||
};
|
||||
|
||||
const closeWindow: CapabilityHandler = async (_ctx, node, args) => {
|
||||
const selector = text(args, ["window_obj", "title", "target_window", "window_title"]);
|
||||
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 powershell(`$p = Get-Process | Where-Object { $_.MainWindowTitle -and $_.MainWindowTitle -like ${psString(`*${selector}*`)} } | Select-Object -First 1; if (-not $p) { throw '找不到窗口' }; $p.CloseMainWindow() | Out-Null; $true`);
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("PDF high-level capabilities", () => {
|
||||
const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
||||
const opened = await pdfjs.getDocument({ data: new Uint8Array(await readFile(encryptedPath)), password: "secret" }).promise;
|
||||
expect(opened.numPages).toBe(1);
|
||||
});
|
||||
}, 60000);
|
||||
});
|
||||
async function mkdtempSafe(): Promise<string> {
|
||||
const directory = await (await import("node:fs/promises")).mkdtemp(path.join(os.tmpdir(), "rpa-pdf-high-"));
|
||||
|
||||
+19
-1
@@ -1,5 +1,6 @@
|
||||
import { app } from "electron";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { FlowDefinition, FlowEdge, FlowNode, FlowSummary } from "../shared/types";
|
||||
|
||||
@@ -71,12 +72,29 @@ export function createFlow(name = "未命名流程"): FlowDefinition {
|
||||
};
|
||||
}
|
||||
|
||||
/** 演示流程浏览器类型:优先 Chrome,未安装时回退 Edge;两者都缺失则保持 chrome 由能力层给出友好错误 */
|
||||
function demoBrowserType(): string {
|
||||
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
||||
const chromePaths = [
|
||||
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
||||
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
||||
path.join(localAppData, "Google", "Chrome", "Application", "chrome.exe")
|
||||
];
|
||||
if (chromePaths.some((p) => fs.existsSync(p))) return "chrome";
|
||||
const edgePaths = [
|
||||
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
||||
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe"
|
||||
];
|
||||
if (edgePaths.some((p) => fs.existsSync(p))) return "edge";
|
||||
return "chrome";
|
||||
}
|
||||
|
||||
export function createDemoFlow(url: string): FlowDefinition {
|
||||
const flow = createFlow("本地网页订单演示");
|
||||
flow.description = "打开本地演示站,填写客户和金额,提交订单并读取结果。";
|
||||
flow.nodes = [
|
||||
flow.nodes[0],
|
||||
{ id: "node_open", componentId: "WebBrowser_CreateV3", position: { x: 180, y: 250 }, values: { type: "chrome", url, isRemote: "0", openNewTab: "1", waitingload: "1" }, options: { timeoutMs: 30000 } },
|
||||
{ id: "node_open", componentId: "WebBrowser_CreateV3", position: { x: 180, y: 250 }, values: { type: demoBrowserType(), url, isRemote: "0", openNewTab: "1", waitingload: "1" }, options: { timeoutMs: 30000 } },
|
||||
{ id: "node_name", componentId: "Element_SetValue_V6", position: { x: 180, y: 390 }, values: { uiSelector: "css:#name", designated_info: "0", designated_obj: "", text: "RPA 客户", input_type: "0", isEnter: "0", interval: "0" }, options: { timeoutMs: 10000 } },
|
||||
{ id: "node_amount", componentId: "Element_SetValue_V6", position: { x: 180, y: 530 }, values: { uiSelector: "css:#amount", designated_info: "0", designated_obj: "", text: "128", input_type: "0", isEnter: "0", interval: "0" }, options: { timeoutMs: 10000 } },
|
||||
{ id: "node_submit", componentId: "Element_Click_V6", position: { x: 180, y: 670 }, values: { uiSelector: "css:#submit", designated_info: "0", designated_obj: "", clickway: "0", clickfunc: "0", operation: "1" }, options: { timeoutMs: 10000 } },
|
||||
|
||||
@@ -636,14 +636,18 @@ function FieldControl({
|
||||
onToast("未找到可见窗口", "error");
|
||||
return;
|
||||
}
|
||||
const numbered = list.map((w: { title: string }, i: number) => (i + 1) + ". " + w.title).join("\n");
|
||||
const choice = prompt("选择窗口(输入编号):\n" + numbered);
|
||||
const idx = Number(choice) - 1;
|
||||
if (Number.isInteger(idx) && list[idx]) {
|
||||
onChange(field.key, list[idx].title);
|
||||
onToast("已选择窗口:" + list[idx].title);
|
||||
for (let wi = 0; wi < list.length; wi++) {
|
||||
const w = list[wi] as { title: string; process?: string };
|
||||
const ok = confirm(`窗口 ${wi + 1}/${list.length}:${w.title}(${w.process ?? "未知"})\n\n确定选这个窗口?取消看下一个`);
|
||||
if (ok) {
|
||||
onChange(field.key, w.title);
|
||||
onToast("已选择窗口:" + w.title);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
onToast("未选择窗口");
|
||||
}
|
||||
catch (error) {
|
||||
onToast(error instanceof Error ? error.message : String(error), "error");
|
||||
} finally {
|
||||
setPicking(false);
|
||||
@@ -673,11 +677,15 @@ function FieldControl({
|
||||
? "填写完整文件路径,或点右侧按钮选择"
|
||||
: field.widget === "folder"
|
||||
? "填写完整文件夹路径,或点右侧按钮选择"
|
||||
: field.widget === "region" && (field.displayType === "captureWindow" || field.displayType === "openWindowSelect")
|
||||
? "填写窗口标题,或点「选窗口」从可见窗口列表中选择"
|
||||
|
||||
: field.widget === "region"
|
||||
? field.displayType === "screenShot"
|
||||
? "填模板图片路径,或点「屏幕框选」截取屏幕上的目标图案"
|
||||
: "填写区域坐标 JSON,或点「屏幕框选」生成"
|
||||
: "";
|
||||
|
||||
const hint = field.description || fallbackHint;
|
||||
|
||||
return (
|
||||
@@ -778,12 +786,27 @@ function FieldControl({
|
||||
placeholder={field.placeholder || ""}
|
||||
onChange={(event) => onChange(field.key, event.target.value)}
|
||||
/>
|
||||
) : field.widget === "region" && (field.displayType === "captureWindow" || field.displayType === "openWindowSelect") ? (
|
||||
<div className="field-row">
|
||||
<input
|
||||
className="text-input"
|
||||
value={textOf(raw)}
|
||||
|
||||
placeholder={field.placeholder || "点右侧「选窗口」从可见窗口列表中选择"}
|
||||
onChange={(event) => onChange(field.key, event.target.value)}
|
||||
/>
|
||||
<VarMenu variables={variables} onPick={insertVariable} />
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickWindowToField()} title="从当前可见窗口列表中选择">
|
||||
{picking ? "选择中" : "选窗口"}
|
||||
</button>
|
||||
</div>
|
||||
) : field.widget === "region" && field.displayType === "screenShot" ? (
|
||||
// 模板图片类字段(Image_* 的图片路径等): 值是文件路径, 框选是把截图存盘后填路径
|
||||
<div className="field-row">
|
||||
<input
|
||||
className="text-input"
|
||||
value={textOf(raw)}
|
||||
|
||||
placeholder={field.placeholder || "填模板图片路径,或点「屏幕框选」截取"}
|
||||
onChange={(event) => onChange(field.key, event.target.value)}
|
||||
/>
|
||||
@@ -817,6 +840,21 @@ function FieldControl({
|
||||
onChange={(event) => onChange(field.key, event.target.value)}
|
||||
/>
|
||||
<VarMenu variables={variables} onPick={insertVariable} />
|
||||
{isWindowTarget && /window_obj|match_title|title|window/i.test(field.key) && (
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickWindowToField()} title="从当前可见窗口列表中选择">
|
||||
{picking ? "选择中" : "选窗口"}
|
||||
</button>
|
||||
)}
|
||||
{field.displayType === "imageSelector" && (
|
||||
<>
|
||||
<button type="button" className="mini-btn" onClick={() => void pickPath(false)} title="选择图片文件">
|
||||
<FolderOpen className="icon" style={{ width: 13, height: 13 }} />
|
||||
</button>
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickRegionToField()} title="屏幕截取区域作为图片">
|
||||
{picking ? "截取中" : "屏幕截取"}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -29,6 +29,20 @@ export function fieldIsActive(componentId: string, key: string, values: Record<s
|
||||
}
|
||||
}
|
||||
if (componentId === "System_ScreenSaverShowV1" && key === "password") return truthy(values.set_password);
|
||||
// Window_*_v1 series: conditional fields based on get_type mode
|
||||
if (/^Window_.*_v1$/.test(componentId) || componentId === "Window_WindowExists" || componentId === "Window_WaitWindow") {
|
||||
const gt = String(values.get_type ?? "");
|
||||
if (gt !== "") {
|
||||
if (key === "target_window") return gt === "0";
|
||||
if (key === "window_obj") return gt === "1";
|
||||
if (key === "match_type" || key === "select_title" || key === "match_title") return gt === "2";
|
||||
}
|
||||
if (gt === "2") {
|
||||
const mt = String(values.match_type ?? "0");
|
||||
if (key === "select_title") return mt === "0";
|
||||
if (key === "match_title") return mt === "1";
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user