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] =?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],