fix: 窗口组件条件字段渲染+选窗口按钮; imageSelector加文件浏览与屏幕截取; prompt()改confirm(); 后端窗口selector统一读取所有标题字段; 新增桌面键鼠人工验收流程与E2E脚本

This commit is contained in:
yu committed 2026-11-03 13:12:29 +08:00
1 parent 68e16282de
commit b5d6bef369
9 files changed
+373 -17

No files matched your search

+2 -2
View File
@@ -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) {
+24 -4
View File
@@ -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`);
};
+1 -1
View File
@@ -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
View File
@@ -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 } },
+45 -7
View File
@@ -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;
}