From 241c598b10960c99420b5ec8bb83c4499ce539dd Mon Sep 17 00:00:00 2001
From: yu <191972707@qq.com>
Date: Tue, 3 Nov 2026 13:16:34 +0800
Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=E5=8C=B9=E9=85=8D=E9=98=88=E5=80=BC?=
=?UTF-8?q?=E5=88=A4=E5=AE=9A=E7=A7=BB=E5=88=B0=E7=B2=BE=E5=8C=96=E5=90=8E?=
=?UTF-8?q?=E5=B9=B6=E5=8F=96=E6=B6=88=E5=88=9D=E5=A7=8B=E5=89=AA=E6=9E=9D?=
=?UTF-8?q?,=20=E4=BF=AE=E5=A4=8D=E9=AB=98=E7=9B=B8=E4=BC=BC=E5=BA=A6(0.95?=
=?UTF-8?q?)=E8=AF=AF=E5=88=A4=E6=9C=AA=E6=89=BE=E5=88=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/main/capabilities/vision.ts | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/src/main/capabilities/vision.ts b/src/main/capabilities/vision.ts
index a8e893b..c90d02e 100644
--- a/src/main/capabilities/vision.ts
+++ b/src/main/capabilities/vision.ts
@@ -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;
}
From 12b97315f22eb3ed407cfc999da6b8ccf59749ca Mon Sep 17 00:00:00 2001
From: yu <191972707@qq.com>
Date: Tue, 3 Nov 2026 13:28:49 +0800
Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E7=AA=97=E5=8F=A3=E5=AF=B9=E8=B1=A1?=
=?UTF-8?q?=E8=AF=AD=E4=B9=89=E5=AD=97=E6=AE=B5(window=5Fobj/=E7=AA=97?=
=?UTF-8?q?=E5=8F=A3=E5=AF=B9=E8=B1=A1)=E7=BB=9F=E4=B8=80=E6=B8=B2?=
=?UTF-8?q?=E6=9F=93=E9=80=89=E7=AA=97=E5=8F=A3=E6=8C=89=E9=92=AE,=20?=
=?UTF-8?q?=E4=B8=8D=E5=86=8D=E4=BE=9D=E8=B5=96=E7=BB=84=E4=BB=B6=E5=89=8D?=
=?UTF-8?q?=E7=BC=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/renderer/src/panels/Properties.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/renderer/src/panels/Properties.tsx b/src/renderer/src/panels/Properties.tsx
index 7482ed3..91a4c8d 100644
--- a/src/renderer/src/panels/Properties.tsx
+++ b/src/renderer/src/panels/Properties.tsx
@@ -840,7 +840,9 @@ function FieldControl({
onChange={(event) => onChange(field.key, event.target.value)}
/>
- {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))) && (
From 6027895d4de86a2cf7b33d5f26a78aadbf81b1a3 Mon Sep 17 00:00:00 2001
From: yu <191972707@qq.com>
Date: Tue, 3 Nov 2026 13:34:42 +0800
Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=E7=82=B9=E5=87=BB=E5=9B=BE?=
=?UTF-8?q?=E5=83=8F=E7=BB=84=E4=BB=B6=E5=90=88(V2=E5=B9=B6=E5=85=A5V3):?=
=?UTF-8?q?=20=E7=B2=BE=E7=AE=80=E4=B8=BA=E6=9F=A5=E6=89=BE=E8=8C=83?=
=?UTF-8?q?=E5=9B=B4/=E7=AA=97=E5=8F=A3=E5=AF=B9=E8=B1=A1/=E7=9B=AE?=
=?UTF-8?q?=E6=A0=87=E5=9B=BE=E5=83=8F/=E6=8C=89=E9=94=AE/=E4=BD=8D?=
=?UTF-8?q?=E7=BD=AE/=E7=9B=B8=E4=BC=BC=E5=BA=A6=E4=B8=83=E5=AD=97?=
=?UTF-8?q?=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
resources/capability-duplicates.json | 11 +++----
resources/catalog.json | 44 +++++-----------------------
2 files changed, 13 insertions(+), 42 deletions(-)
diff --git a/resources/capability-duplicates.json b/resources/capability-duplicates.json
index 02a211b..0f662eb 100644
--- a/resources/capability-duplicates.json
+++ b/resources/capability-duplicates.json
@@ -3,9 +3,9 @@
"generatedAt": "2026-11-02T04:56:17.341Z",
"policy": "同名能力按目标域、协议和最新版本保留主能力;重复旧版本仅隐藏,不删除执行器",
"editableBefore": 894,
- "keep": 658,
- "hide": 236,
- "editableAfter": 658,
+ "keep": 657,
+ "hide": 237,
+ "editableAfter": 657,
"decisions": [
{
"id": "Trigger.Manual",
@@ -3308,8 +3308,9 @@
{
"id": "Image_Click_V2",
"name": "点击图像",
- "status": "keep",
- "reason": "目标域 / 协议内的最新主版本"
+ "status": "hide",
+ "reason": "与 V3 功能重复合并, V3 为统一版本(支持整个屏幕/指定窗口查找)",
+ "canonical": "Image_Click_V3"
},
{
"id": "Image_Click_V1",
diff --git a/resources/catalog.json b/resources/catalog.json
index 3cf332f..2b2d93e 100644
--- a/resources/catalog.json
+++ b/resources/catalog.json
@@ -68545,7 +68545,7 @@
},
{
"id": "Image_Click_V3",
- "name": "鼠标点击图像",
+ "name": "点击图像",
"kind": "vendor",
"group": "vision",
"categories": [
@@ -68608,10 +68608,6 @@
{
"label": "指定窗口",
"value": "2"
- },
- {
- "label": "指定区域",
- "value": "3"
}
]
},
@@ -68621,41 +68617,15 @@
"widget": "text",
"type": "string",
"displayType": "text-expression",
- "required": true,
+ "required": false,
"defaultValue": "",
"placeholder": "",
- "description": "目标窗口对象;留空则作用于当前活动窗口",
+ "description": "目标窗口标题,留空则作用于当前活动窗口;点「选窗口」从可见窗口列表中选择",
"advanced": false,
"valueTypes": [
"rpa_window_new"
]
},
- {
- "key": "action",
- "label": "action",
- "widget": "region",
- "type": "object",
- "displayType": "getScreenShot",
- "required": false,
- "defaultValue": "",
- "placeholder": "{\"x\":0,\"y\":0,\"width\":100,\"height\":50}",
- "description": "区域坐标 JSON;点右侧「屏幕框选」在屏幕上框选生成",
- "advanced": false,
- "valueTypes": []
- },
- {
- "key": "shot_position",
- "label": "shot_position",
- "widget": "region",
- "type": "object",
- "displayType": "screenShotValue",
- "required": false,
- "defaultValue": "",
- "placeholder": "{\"x\":0,\"y\":0,\"width\":100,\"height\":50}",
- "description": "区域坐标 JSON;点右侧「屏幕框选」在屏幕上框选生成",
- "advanced": false,
- "valueTypes": []
- },
{
"key": "image",
"label": "目标图像",
@@ -68748,13 +68718,13 @@
{
"key": "accuracy",
"label": "相似度",
- "widget": "hidden",
+ "widget": "similaritySlider",
"type": "string",
- "displayType": "hidden",
+ "displayType": "similaritySlider",
"required": true,
- "defaultValue": "0.95",
+ "defaultValue": "0.9",
"placeholder": "请输入相似度阈值",
- "description": "模板相似度阈值(0~1,越大越严格):0.9 表示屏幕上至少 90% 像素与模板一致才算找到。框选的模板与实际界面光照/渲染略有差异时可适当调低,如无特殊需求建议 0.8。",
+ "description": "模板相似度阈值(0~1,越大越严格):0.9 表示屏幕上至少 90% 像素与模板一致才算找到。建议 0.8~0.9。",
"advanced": false,
"valueTypes": [
"rpa_num"
From d4f27cdaab6ff87f9c747d520f1a1f3ec219fb05 Mon Sep 17 00:00:00 2001
From: yu <191972707@qq.com>
Date: Tue, 3 Nov 2026 13:44:59 +0800
Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20=E6=89=BE=E5=9B=BE=E6=97=8F?=
=?UTF-8?q?=E7=BB=84=E4=BB=B6=E7=BB=9F=E4=B8=80=E6=96=B0=E7=AE=A1=E7=BA=BF?=
=?UTF-8?q?(Exists/Location/Wait/Move=E8=BF=81=E8=87=B3=E8=99=9A=E6=8B=9F?=
=?UTF-8?q?=E6=A1=8C=E9=9D=A2+=E7=9B=B8=E4=BC=BC=E5=BA=A6=E8=AF=AD?=
=?UTF-8?q?=E4=B9=89),=20=E9=9D=A2=E6=9D=BF=E5=8F=AA=E7=95=99V1/V2?=
=?UTF-8?q?=E6=9C=80=E6=96=B0=E7=89=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
resources/capability-duplicates.json | 31 +++---
resources/catalog.json | 137 ++++++++++--------------
src/main/capabilities/remaining-core.ts | 20 +---
src/main/capabilities/vision.ts | 45 ++++++++
4 files changed, 122 insertions(+), 111 deletions(-)
diff --git a/resources/capability-duplicates.json b/resources/capability-duplicates.json
index 0f662eb..86f97b0 100644
--- a/resources/capability-duplicates.json
+++ b/resources/capability-duplicates.json
@@ -3,9 +3,9 @@
"generatedAt": "2026-11-02T04:56:17.341Z",
"policy": "同名能力按目标域、协议和最新版本保留主能力;重复旧版本仅隐藏,不删除执行器",
"editableBefore": 894,
- "keep": 657,
- "hide": 237,
- "editableAfter": 657,
+ "keep": 654,
+ "hide": 240,
+ "editableAfter": 654,
"decisions": [
{
"id": "Trigger.Manual",
@@ -3355,7 +3355,7 @@
"name": "查找图像",
"canonical": "Image_Find_V1",
"status": "hide",
- "reason": "同名旧版本或重复别名,保留执行兼容"
+ "reason": "同名旧版本, 保留执行兼容"
},
{
"id": "Image_Base64Encode",
@@ -3374,7 +3374,7 @@
"name": "等待图像",
"canonical": "Image_Wait_V1",
"status": "hide",
- "reason": "同名旧版本或重复别名,保留执行兼容"
+ "reason": "同名旧版本, 保留执行兼容"
},
{
"id": "Image_Exists_V1",
@@ -3387,19 +3387,21 @@
"name": "判断图像",
"canonical": "Image_Exists_V1",
"status": "hide",
- "reason": "同名旧版本或重复别名,保留执行兼容"
+ "reason": "同名旧版本, 保留执行兼容"
},
{
"id": "Image_Location",
"name": "图像位置",
- "status": "keep",
- "reason": "唯一能力"
+ "status": "hide",
+ "reason": "与查找图像功能重复, 统一由 Find_V1 提供坐标返回",
+ "canonical": "Image_Find_V1"
},
{
"id": "Image_Location_V1",
"name": "获取图像位置",
- "status": "keep",
- "reason": "唯一能力"
+ "status": "hide",
+ "reason": "与查找图像功能重复, 统一由 Find_V1 提供坐标返回",
+ "canonical": "Image_Find_V1"
},
{
"id": "Image_CropImg",
@@ -3422,15 +3424,16 @@
{
"id": "Image_Move_V1",
"name": "移动到图像",
- "status": "keep",
- "reason": "目标域 / 协议内的最新主版本"
+ "status": "hide",
+ "reason": "同名旧版本, 保留执行兼容",
+ "canonical": "Image_Move_V2"
},
{
"id": "Image_Move",
"name": "移动到图像",
- "canonical": "Image_Move_V1",
+ "canonical": "Image_Move_V2",
"status": "hide",
- "reason": "同名旧版本或重复别名,保留执行兼容"
+ "reason": "同名旧版本, 保留执行兼容"
},
{
"id": "Ini_DeleteKey",
diff --git a/resources/catalog.json b/resources/catalog.json
index 2b2d93e..e371dc0 100644
--- a/resources/catalog.json
+++ b/resources/catalog.json
@@ -68050,14 +68050,9 @@
"key": "find_range",
"label": "图像查找范围",
"widget": "select",
- "type": "string",
"displayType": "dropDownSelect",
- "required": false,
"defaultValue": "1",
- "placeholder": "",
- "description": "请选择查找目标图像的范围",
- "advanced": false,
- "valueTypes": [],
+ "required": false,
"options": [
{
"label": "整个屏幕",
@@ -68066,83 +68061,36 @@
{
"label": "指定窗口",
"value": "2"
- },
- {
- "label": "指定区域",
- "value": "3"
}
- ]
+ ],
+ "description": "在整个虚拟桌面(含副屏)中查找,或仅在指定窗口范围内查找"
},
{
"key": "window_obj",
"label": "窗口对象",
"widget": "text",
- "type": "string",
"displayType": "text-expression",
- "required": true,
"defaultValue": "",
- "placeholder": "",
- "description": "目标窗口对象;留空则作用于当前活动窗口",
- "advanced": false,
- "valueTypes": [
- "rpa_window_new"
- ]
- },
- {
- "key": "action",
- "label": "action",
- "widget": "region",
- "type": "object",
- "displayType": "getScreenShot",
"required": false,
- "defaultValue": "",
- "placeholder": "{\"x\":0,\"y\":0,\"width\":100,\"height\":50}",
- "description": "区域坐标 JSON;点右侧「屏幕框选」在屏幕上框选生成",
- "advanced": false,
- "valueTypes": []
- },
- {
- "key": "position",
- "label": "position",
- "widget": "region",
- "type": "object",
- "displayType": "screenShotValue",
- "required": false,
- "defaultValue": "",
- "placeholder": "{\"x\":0,\"y\":0,\"width\":100,\"height\":50}",
- "description": "区域坐标 JSON;点右侧「屏幕框选」在屏幕上框选生成",
- "advanced": false,
- "valueTypes": []
+ "description": "目标窗口标题,留空则作用于当前活动窗口;点「选窗口」从可见窗口列表中选择"
},
{
"key": "image",
"label": "目标图像",
"widget": "text",
- "type": "string",
"displayType": "imageSelector",
- "required": true,
"defaultValue": "",
- "placeholder": "截取或选择图像",
- "description": "图片文件的完整路径,用于匹配或识别;点右侧按钮选择文件",
- "advanced": false,
- "valueTypes": [
- "rpa_str"
- ]
+ "required": true,
+ "description": "模板图片文件的完整路径,用于匹配识别;点右侧「屏幕截取」可框选屏幕上的目标图案"
},
{
"key": "accuracy",
- "label": "相似度",
- "widget": "hidden",
- "type": "string",
- "displayType": "hidden",
+ "label": "模板相似度",
+ "widget": "similaritySlider",
+ "displayType": "similaritySlider",
+ "defaultValue": "0.9",
"required": true,
- "defaultValue": "0.95",
- "placeholder": "请输入相似度阈值",
- "description": "模板相似度阈值(0~1,越大越严格):0.9 表示屏幕上至少 90% 像素与模板一致才算找到。框选的模板与实际界面光照/渲染略有差异时可适当调低,如无特殊需求建议 0.8。",
- "advanced": false,
- "valueTypes": [
- "rpa_num"
- ]
+ "description": "相似度阈值(0~1,越大越严格):0.9 表示屏幕上至少 90% 像素与模板一致才算找到。建议 0.8~0.9。"
}
],
"output": {
@@ -68199,30 +68147,59 @@
"risk": "low",
"fields": [
{
- "key": "image",
- "label": "图片路径",
- "widget": "region",
- "type": "object",
- "displayType": "screenShot",
+ "key": "find_range",
+ "label": "图像查找范围",
+ "widget": "select",
+ "displayType": "dropDownSelect",
+ "defaultValue": "1",
"required": false,
+ "options": [
+ {
+ "label": "整个屏幕",
+ "value": "1"
+ },
+ {
+ "label": "指定窗口",
+ "value": "2"
+ }
+ ],
+ "description": "在整个虚拟桌面(含副屏)中查找,或仅在指定窗口范围内查找"
+ },
+ {
+ "key": "window_obj",
+ "label": "窗口对象",
+ "widget": "text",
+ "displayType": "text-expression",
"defaultValue": "",
- "placeholder": "",
- "description": "图片文件的完整路径,用于匹配或识别;点右侧按钮选择文件",
- "advanced": false,
- "valueTypes": []
+ "required": false,
+ "description": "目标窗口标题,留空则作用于当前活动窗口;点「选窗口」从可见窗口列表中选择"
+ },
+ {
+ "key": "image",
+ "label": "目标图像",
+ "widget": "text",
+ "displayType": "imageSelector",
+ "defaultValue": "",
+ "required": true,
+ "description": "模板图片文件的完整路径,用于匹配识别;点右侧「屏幕截取」可框选屏幕上的目标图案"
},
{
"key": "accuracy",
- "label": "相似度",
- "widget": "text",
- "type": "string",
+ "label": "模板相似度",
+ "widget": "similaritySlider",
"displayType": "similaritySlider",
+ "defaultValue": "0.9",
+ "required": true,
+ "description": "相似度阈值(0~1,越大越严格):0.9 表示屏幕上至少 90% 像素与模板一致才算找到。建议 0.8~0.9。"
+ },
+ {
+ "key": "timeout",
+ "label": "等待超时(秒)",
+ "widget": "number",
+ "displayType": "text-expression",
+ "defaultValue": "0",
"required": false,
- "defaultValue": "0.8",
- "placeholder": "输入内容或 ${变量}",
- "description": "模板相似度阈值(0~1,越大越严格):0.9 表示屏幕上至少 90% 像素与模板一致才算找到。框选的模板与实际界面光照/渲染略有差异时可适当调低,如无特殊需求建议 0.8。",
- "advanced": false,
- "valueTypes": []
+ "description": "大于 0 时在该时间内每隔 0.3 秒重试查找,超时仍未找到才返回 false"
}
],
"output": {
diff --git a/src/main/capabilities/remaining-core.ts b/src/main/capabilities/remaining-core.ts
index 368de56..61956d9 100644
--- a/src/main/capabilities/remaining-core.ts
+++ b/src/main/capabilities/remaining-core.ts
@@ -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, 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`);
diff --git a/src/main/capabilities/vision.ts b/src/main/capabilities/vision.ts
index c90d02e..77f4849 100644
--- a/src/main/capabilities/vision.ts
+++ b/src/main/capabilities/vision.ts
@@ -436,6 +436,51 @@ async function locate(args: Record, 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 = {
center: [0.5, 0.5],