Merge branch 'main' of http://43.140.215.244:7040/wangyukun/PRA-only-demo
This commit is contained in:
commit
6b54decfb0
5 files changed
+148
-154
No files matched your search
@@ -158,27 +158,13 @@ register("GoogleTable_RenameSheet", async (_ctx, _node, args) => {
|
||||
const sheet = (out.sheets || []).find((item: any) => item.properties.title === oldName); if (!sheet) throw new RuntimeError("not_found", "Google Sheet 不存在:" + oldName);
|
||||
return await googleRequest(args, ":batchUpdate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ requests: [{ updateSheet: { properties: { sheetId: sheet.properties.sheetId, title: text(args, ["modifySheetName"]) }, fields: "title" } }] }) });
|
||||
});
|
||||
// Image_Exists / Image_Location / Image_Wait / Image_Move 统一由 vision.ts 注册到新查找管线
|
||||
// (虚拟桌面多屏拼接 + 相似度语义 + JPG 模板)。此处旧实现用 GDI CopyFromScreen 只截主屏
|
||||
// 且 accuracy 语义颠倒, 已整体迁出删除。
|
||||
async function captureScreen(file: string) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
await powershell(`Add-Type -AssemblyName System.Drawing;Add-Type -AssemblyName System.Windows.Forms;$bounds=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds;$bitmap=New-Object System.Drawing.Bitmap $bounds.Width,$bounds.Height;$graphics=[System.Drawing.Graphics]::FromImage($bitmap);$graphics.CopyFromScreen($bounds.Location,[System.Drawing.Point]::Empty,$bounds.Size);$bitmap.Save(${psString(file)});$graphics.Dispose();$bitmap.Dispose()`);
|
||||
}
|
||||
async function findOnScreen(args: Record<string, unknown>, node: { id: string; componentId: string }, timeout = 0) {
|
||||
const template = path.resolve(text(args, ["image", "image_path", "template"]));
|
||||
if (!fs.existsSync(template)) throw new RuntimeError("validation_error", "模板图片不存在", { nodeId: node.id, componentId: node.componentId, detail: template });
|
||||
const screenshot = path.join(process.env.TEMP || process.cwd(), `image-find-${Date.now()}.png`); const deadline = Date.now() + timeout * 1000;
|
||||
do { await captureScreen(screenshot); const hit = await findImage(template, screenshot, num(args, ["accuracy", "accuracy2"], 0.2));
|
||||
if (hit) { try { fs.unlinkSync(screenshot); } catch {} return hit; } if (timeout > 0) await new Promise((resolve) => setTimeout(resolve, 300)); } while (Date.now() < deadline);
|
||||
try { fs.unlinkSync(screenshot); } catch {} throw new RuntimeError("not_found", "未找到目标图像", { nodeId: node.id, componentId: node.componentId });
|
||||
}
|
||||
for (const id of ["Image_Exists", "Image_Exists_V1"]) register(id, async (_ctx, node, args) => { try { return !!(await findOnScreen(args, node, num(args, ["timeout", "time_out"], 0))); } catch { return false; } });
|
||||
for (const id of ["Image_Location", "Image_Location_V1"]) register(id, async (_ctx, node, args) => findOnScreen(args, node, num(args, ["timeout", "time_out"], 0)));
|
||||
for (const id of ["Image_Wait", "Image_Wait_V1"]) register(id, async (_ctx, node, args) => findOnScreen(args, node, num(args, ["timeout", "time_out"], 10)));
|
||||
for (const id of ["Image_Move", "Image_Move_V1", "Image_Move_V2"]) register(id, async (_ctx, node, args) => {
|
||||
const hit = await findOnScreen(args, node); const x = Math.round(hit.x + hit.width / 2); const y = Math.round(hit.y + hit.height / 2);
|
||||
// 注意: PowerShell here-string(@'...'@)的 @' 后必须紧跟换行, 不能拼成单行; 用 -MemberDefinition 内联声明避开该语法
|
||||
await powershell("Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X, int Y);' -Name RpaImageMoveInput -Namespace RpaImageMove | Out-Null; [RpaImageMove.RpaImageMoveInput]::SetCursorPos(" + x + "," + y + ")");
|
||||
return { ...hit, x, y };
|
||||
});
|
||||
register("Image_Base64Encode", async (_ctx, _node, args) => fs.readFileSync(path.resolve(text(args, ["path", "image"]))).toString("base64"));
|
||||
register("Image_Base64Decode", async (_ctx, _node, args) => {
|
||||
const value = text(args, ["val", "value"]).replace(/^data:[^;]+;base64,/, ""); const file = path.resolve(process.cwd(), `image-${Date.now()}.png`);
|
||||
|
||||
@@ -307,12 +307,19 @@ function scanBest(screen: PNG, template: PNG, maximumDifference: number): MatchR
|
||||
let best: MatchResult | null = null;
|
||||
for (let y = 0; y <= screen.height - template.height; y += scanStep) {
|
||||
for (let x = 0; x <= screen.width - template.width; x += scanStep) {
|
||||
const difference = pixelDifference(screen, template, x, y, sampleStep, best ? 1 - best.score : maximumDifference);
|
||||
// 尚无 best 时不剪枝(完整评估): 若初始就按 maximumDifference 剪枝,
|
||||
// 均匀背景位置在少数采样后返回"刚好压线"的部分均值抢占 best,
|
||||
// 而真位置(常不在网格上)附近的网格点因部分重叠被剪, 精化永远够不到——
|
||||
// 相似度 0.95 时实测 score=1.0 的真位置被判"未达到阈值"
|
||||
const cutoff = best ? 1 - best.score : 1;
|
||||
const difference = pixelDifference(screen, template, x, y, sampleStep, cutoff);
|
||||
const score = 1 - difference;
|
||||
if (!best || score > best.score) best = { x, y, width: template.width, height: template.height, score };
|
||||
}
|
||||
}
|
||||
if (!best || 1 - best.score > maximumDifference) return null;
|
||||
if (!best) return null;
|
||||
// 注意: 阈值判定必须放在精化之后——粗扫网格(步长=模板短边/12)通常够不到精确位置,
|
||||
// 粗扫 best 常差在阈值边缘, 精化后才能提到满分; 先判阈值会把真命中误杀
|
||||
const refine = Math.max(1, scanStep);
|
||||
const minX = Math.max(0, best.x - refine);
|
||||
const maxX = Math.min(screen.width - template.width, best.x + refine);
|
||||
@@ -320,11 +327,14 @@ function scanBest(screen: PNG, template: PNG, maximumDifference: number): MatchR
|
||||
const maxY = Math.min(screen.height - template.height, best.y + refine);
|
||||
for (let y = minY; y <= maxY; y += 1) {
|
||||
for (let x = minX; x <= maxX; x += 1) {
|
||||
const difference = pixelDifference(screen, template, x, y, sampleStep, 1 - best.score);
|
||||
// 精化区域只有几百个位置, 完整评估避免同样的剪枝偏置
|
||||
const difference = pixelDifference(screen, template, x, y, sampleStep, 1);
|
||||
const score = 1 - difference;
|
||||
if (score > best.score) best = { x, y, width: template.width, height: template.height, score };
|
||||
}
|
||||
}
|
||||
// 阈值判定: 精化后的最终得分才与 maximumDifference 比较
|
||||
if (1 - best.score > maximumDifference) return null;
|
||||
return best;
|
||||
}
|
||||
|
||||
@@ -426,6 +436,51 @@ async function locate(args: Record<string, unknown>, evidenceDir: string, nodeId
|
||||
}
|
||||
|
||||
register(["Image_Find", "Image_Find_V1"], async (ctx, node, args) => locate(args, ctx.evidenceDir, node.id));
|
||||
// 图像位置: 与查找图像同义(历史重复组件), 同管线返回坐标
|
||||
for (const id of ["Image_Location", "Image_Location_V1"]) register(id, async (ctx, node, args) => locate(args, ctx.evidenceDir, node.id));
|
||||
// 判断图像: 找到返回 true / 找不到返回 false(供条件分支), 仅有 not_found 转为 false;
|
||||
// 模板路径为空/文件不存在等配置错误仍然抛出, 避免静默 false 难排查。
|
||||
// timeout>0 时在窗口内每 300ms 重试(等待图像出现再判断的场景)
|
||||
for (const id of ["Image_Exists", "Image_Exists_V1"]) register(id, async (ctx, node, args) => {
|
||||
const rawTimeout = Number(text(args, ["timeout", "time_out"], "0"));
|
||||
const deadline = Date.now() + (Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : 0) * 1000;
|
||||
for (;;) {
|
||||
try {
|
||||
await locate(args, ctx.evidenceDir, node.id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof RuntimeError && error.code === "not_found") {
|
||||
if (Date.now() >= deadline) return false;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
}
|
||||
});
|
||||
// 等待图像: 在超时窗口内每 300ms 重试一次查找, 命中即返回坐标, 超时仍无则抛 not_found
|
||||
for (const id of ["Image_Wait", "Image_Wait_V1"]) register(id, async (ctx, node, args) => {
|
||||
const rawTimeout = Number(text(args, ["timeout", "time_out"], "10"));
|
||||
const timeout = Number.isFinite(rawTimeout) && rawTimeout >= 0 ? rawTimeout : 10;
|
||||
const deadline = Date.now() + timeout * 1000;
|
||||
for (;;) {
|
||||
try {
|
||||
return await locate(args, ctx.evidenceDir, node.id);
|
||||
} catch (error) {
|
||||
if (!(error instanceof RuntimeError) || error.code !== "not_found" || Date.now() >= deadline) throw error;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
}
|
||||
});
|
||||
// 移动鼠标到图像: 定位后把光标移到匹配区域中心;
|
||||
// SetCursorPos 传物理坐标, 脚本必须声明 per-monitor DPI 感知(与 injectClick 同理), 否则被系统虚拟化导致偏移
|
||||
for (const id of ["Image_Move", "Image_Move_V1", "Image_Move_V2"]) register(id, async (ctx, node, args) => {
|
||||
const hit = await locate(args, ctx.evidenceDir, node.id);
|
||||
const x = Math.round(hit.x + hit.width / 2);
|
||||
const y = Math.round(hit.y + hit.height / 2);
|
||||
await powershell("Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool SetProcessDpiAwarenessContext(IntPtr value); [DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X, int Y);' -Name RpaImageMoveInput -Namespace RpaImageMove | Out-Null; try { [void][RpaImageMove.RpaImageMoveInput]::SetProcessDpiAwarenessContext([IntPtr](-4)) } catch {}; [RpaImageMove.RpaImageMoveInput]::SetCursorPos(" + x + "," + y + ")");
|
||||
return { ...hit, x, y };
|
||||
});
|
||||
/** 点击位置锚点(相对匹配区域的比例坐标),支持九宫格取值 */
|
||||
const POSITION_ANCHORS: Record<string, [number, number]> = {
|
||||
center: [0.5, 0.5],
|
||||
|
||||
@@ -951,7 +951,9 @@ 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) && (
|
||||
{/* window_obj/窗口对象 等窗口标题语义字段直接给「选窗口」按钮(不依赖组件前缀);
|
||||
其余 Window_ 系组件的字段仍按前缀+关键词匹配 */}
|
||||
{(/^(window_obj|窗口对象|窗口标题|window_title|match_title)$/i.test(field.key) || (isWindowTarget && /window|title/i.test(field.key))) && (
|
||||
<button type="button" className="mini-btn" disabled={picking} onClick={() => void pickWindowToField()} title="从当前可见窗口列表中选择">
|
||||
{picking ? "选择中" : "选窗口"}
|
||||
</button>
|
||||
|
||||
Reference in new issue
Block a user