refactor: 找图族组件统一新管线(Exists/Location/Wait/Move迁至虚拟桌面+相似度语义), 面板只留V1/V2最新版

This commit is contained in:
yu 2026-11-03 13:44:59 +08:00
parent 6027895d4d
commit d4f27cdaab
4 changed files with 122 additions and 111 deletions

View File

@ -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",

View File

@ -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": {

View File

@ -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`);

View File

@ -436,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],