- engine/(registry/types/runtime/expression)物理迁入包, 解耦shared/types(域类型归包, 宿主type-only re-export) - 31个纯Node能力+27个测试迁入包; 宿主留守Electron/浏览器/AI依赖能力, index.ts经@rpa/core/capabilities拉起 - 新增createEngine()/defaultEngine: excel缓存/Excel句柄/db连接/FTP客户端/OCR实例/拟真设置挂引擎实例 - remaining-core子流程依赖改为host-bridge注入, 包内不再import宿主flows - 宿主main/index.ts启动时注入子流程源; electron-vite/vitest/tsconfig配@rpa/core源码别名 - 验证: typecheck绿, 快照909个id diff为零, 332测试过(2个桌面冒烟因会话无交互SetForegroundWindow被拒, 环境阻塞)
84 lines
4.0 KiB
TypeScript
84 lines
4.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { PNG } from "pngjs";
|
||
import fs from "node:fs";
|
||
import os from "node:os";
|
||
import path from "node:path";
|
||
import { findImage } from "./vision";
|
||
|
||
function makeScreen(width: number, height: number, patch: { x: number; y: number; w: number; h: number }): PNG {
|
||
const png = new PNG({ width, height });
|
||
for (let y = 0; y < height; y++) {
|
||
for (let x = 0; x < width; x++) {
|
||
const i = (y * width + x) * 4;
|
||
const inPatch = x >= patch.x && x < patch.x + patch.w && y >= patch.y && y < patch.y + patch.h;
|
||
// 背景: 平滑斜向渐变(贴近真实界面); 补丁: 纯红色
|
||
png.data[i] = inPatch ? 220 : (x + y) % 90;
|
||
png.data[i + 1] = inPatch ? 30 : (x + y + 30) % 90;
|
||
png.data[i + 2] = inPatch ? 40 : (x + y + 60) % 90;
|
||
png.data[i + 3] = 255;
|
||
}
|
||
}
|
||
return png;
|
||
}
|
||
|
||
function crop(source: PNG, x: number, y: number, w: number, h: number): PNG {
|
||
const out = new PNG({ width: w, height: h });
|
||
PNG.bitblt(source, out, x, y, w, h, 0, 0);
|
||
return out;
|
||
}
|
||
|
||
describe("findImage 匹配质量", () => {
|
||
it("相似度0.9时精确命中模板位置且不误报", async () => {
|
||
const patch = { x: 123, y: 77, w: 48, h: 36 };
|
||
const screen = makeScreen(400, 300, patch);
|
||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "vision-"));
|
||
const screenPath = path.join(dir, "screen.png");
|
||
const templatePath = path.join(dir, "template.png");
|
||
fs.writeFileSync(screenPath, PNG.sync.write(screen));
|
||
fs.writeFileSync(templatePath, PNG.sync.write(crop(screen, patch.x, patch.y, patch.w, patch.h)));
|
||
// 相似度 0.9 → 最大差异 0.1
|
||
const result = await findImage(templatePath, screenPath, 0.1);
|
||
expect(result).not.toBeNull();
|
||
// 采样步长落在整数边界时, 相邻1px偏移可能同样满分, 允许±1px
|
||
expect(Math.abs(result!.x - patch.x)).toBeLessThanOrEqual(1);
|
||
expect(Math.abs(result!.y - patch.y)).toBeLessThanOrEqual(1);
|
||
expect(result!.score).toBeGreaterThanOrEqual(0.9);
|
||
// 无该补丁的屏幕应返回 null 而非乱命中
|
||
const other = makeScreen(400, 300, { x: 0, y: 0, w: 1, h: 1 });
|
||
const otherPath = path.join(dir, "other.png");
|
||
fs.writeFileSync(otherPath, PNG.sync.write(other));
|
||
const miss = await findImage(templatePath, otherPath, 0.1);
|
||
expect(miss).toBeNull();
|
||
});
|
||
|
||
it("模板被缩放(1.25倍)时按尺度兜底命中", async () => {
|
||
const patch = { x: 200, y: 120, w: 60, h: 40 };
|
||
const screen = makeScreen(500, 350, patch);
|
||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "vision-"));
|
||
const screenPath = path.join(dir, "screen.png");
|
||
fs.writeFileSync(screenPath, PNG.sync.write(screen));
|
||
// 模板取自 DIP 截图(比物理屏小1.25倍): 把补丁缩小1/1.25后存为模板
|
||
const { createCanvas, loadImage } = await import("@napi-rs/canvas");
|
||
const small = crop(screen, patch.x, patch.y, patch.w, patch.h);
|
||
const srcCanvas = createCanvas(small.width, small.height);
|
||
const srcCtx = srcCanvas.getContext("2d");
|
||
const imgData = srcCtx.createImageData(small.width, small.height);
|
||
imgData.data.set(small.data);
|
||
srcCtx.putImageData(imgData, 0, 0);
|
||
const dstCanvas = createCanvas(Math.round(small.width / 1.25), Math.round(small.height / 1.25));
|
||
const dstCtx = dstCanvas.getContext("2d");
|
||
dstCtx.drawImage(srcCanvas, 0, 0, dstCanvas.width, dstCanvas.height);
|
||
// canvas → pngjs PNG(PNG.sync.write 只认 pngjs 对象)
|
||
const scaledData = dstCtx.getImageData(0, 0, dstCanvas.width, dstCanvas.height);
|
||
const templatePng = new PNG({ width: dstCanvas.width, height: dstCanvas.height });
|
||
templatePng.data = Buffer.from(scaledData.data);
|
||
const templatePath = path.join(dir, "template-small.png");
|
||
fs.writeFileSync(templatePath, PNG.sync.write(templatePng));
|
||
const result = await findImage(templatePath, screenPath, 0.1, [1.25, 1 / 1.25]);
|
||
expect(result).not.toBeNull();
|
||
expect(Math.abs(result!.x - patch.x)).toBeLessThanOrEqual(3);
|
||
expect(Math.abs(result!.y - patch.y)).toBeLessThanOrEqual(3);
|
||
void loadImage;
|
||
});
|
||
});
|