基于 Rust + napi-rs 构建的 Windows 自动化原生 Node.js 模块。涵盖窗口管理、截图、鼠标键盘模拟、进程控制和计算机视觉(以图找图)等能力。
GitHub: https://github.com/qulingoo/win-proxy
npm install
npm run build # release 构建
npm run build:debug # debug 构建const {
// 截图
captureScreen, captureRegion, captureWindow,
// 窗口
findWindow, enumWindows, getForegroundWindow,
// 输入
mouseClick, keyPress, keyTypeText,
// 进程
shellExecute, terminateProcessById,
// CV
findOnScreen, findAllOnScreen, loadImage, saveImage,
// 颜色
getColorOnScreen, findColorOnScreen,
} = require("./index.js");截取全屏,返回 ImageData。
const screen = captureScreen();
console.log(screen.width, screen.height, screen.buffer.length);截取指定屏幕区域。
const region = captureRegion(100, 100, 400, 300);截取指定窗口。hwnd 为窗口句柄(i64)。
const hwnd = findWindow(null, "计算器");
const img = captureWindow(hwnd);返回类型 ImageData:
| 字段 | 类型 | 说明 |
|---|---|---|
width |
number |
图像宽度(像素) |
height |
number |
图像高度(像素) |
buffer |
Buffer |
RGBA 像素数据 |
根据类名或窗口标题查找顶层窗口,返回窗口句柄。未找到返回 0。
const hwnd = findWindow(null, "记事本");
const hwnd = findWindow("Notepad", null);在父窗口内查找子窗口。afterChild 为从前一个子窗口之后开始搜索,传 0 从头搜索。
const child = findWindowEx(parentHwnd, 0, "Edit", null);枚举所有顶层窗口,返回句柄数组。
const handles = enumWindows();枚举指定窗口的所有子窗口。
const children = enumChildWindows(parentHwnd);获取屏幕坐标处的窗口句柄。
获取桌面窗口句柄。
获取当前前台窗口句柄。
获取指定窗口的 Z 序顶部子窗口。
获取与指定窗口有特定关系的窗口。cmd 为 GW_* 常量(如 GW_OWNER = 4, GW_HWNDNEXT = 2)。
获取窗口标题文本。
设置窗口标题文本。
获取窗口类名。
获取窗口在屏幕上的矩形区域,返回 Rect { left, top, right, bottom }。
获取窗口客户区矩形,返回 Rect。
判断句柄是否为有效窗口。
判断窗口是否可见。
判断窗口是否启用(可交互)。
获取父窗口句柄。
获取祖先窗口句柄。flags 为 GA_* 常量(GA_PARENT = 1, GA_ROOT = 2, GA_ROOTOWNER = 3)。
获取窗口所属线程和进程 ID,返回 ThreadProcessId { threadId, processId }。
获取窗口长指针值。index 为 GWLP_* 常量。
设置窗口长指针值。
获取类长指针值。
获取/设置窗口放置信息,返回/接受 WindowPlacement 对象。
const placement = getWindowPlacement(hwnd);
console.log(placement.showCmd); // 1=正常, 2=最小化, 3=最大化控制窗口显示状态。cmd 为 SW_* 常量:
SW_HIDE = 0— 隐藏SW_SHOW = 5— 显示SW_MINIMIZE = 6— 最小化SW_MAXIMIZE = 3— 最大化SW_RESTORE = 9— 还原
将窗口设为前台窗口。
移动并调整窗口大小。repaint 是否重绘。
设置窗口 Z 序和位置。after 可用 HWND_TOPMOST (0xFFFFFFFF) 等值。
最小化窗口(不是关闭,Win32 API 行为)。
将窗口提到 Z 序顶部。
启用/禁用窗口。
设置/获取键盘焦点窗口。
强制窗口更新客户区。
同步发送窗口消息,返回消息结果。
异步投递窗口消息到队列,返回是否成功。
获取光标位置,返回 Point { x, y }。
const pos = getCursorPos();
console.log(pos.x, pos.y);设置光标位置。
获取光标详细信息,返回 CursorInfo { flags, showing, handle, x, y }。
显示/隐藏光标。true 显示,false 隐藏。
按钮编号:0 = 左键,1 = 右键,2 = 中键。
按下鼠标按键。
释放鼠标按键。
单击鼠标按键(按下+释放)。
mouseClick(0); // 左键单击双击鼠标按键。
绝对移动光标到屏幕坐标。
相对当前光标位置移动。
垂直滚轮。正值向上滚动,负值向下滚动。通常步进为 120(WHEEL_DELTA)。
mouseScroll(120); // 向上滚一格
mouseScroll(-120); // 向下滚一格
mouseScroll(360); // 向上滚三格水平滚轮。正值向右,负值向左。
vk 参数为虚拟键码(Virtual Key Code),常见值:
0x08Backspace,0x0DEnter,0x1BEscape0x10Shift,0x11Ctrl,0x12Alt0x20Space,0x2DInsert,0x2EDelete0x41Z,0x5AA0x3090x390VK_F1VK_F12:0x700x7B
按下按键。
释放按键。
按下并释放按键(单击)。
keyPress(0x0D); // 按下 Enter
keyPress(0x1B); // 按下 Escape组合键。先按住所有修饰键,再点击主键,最后释放所有修饰键。
keyCombo([0x11], 0x43); // Ctrl+C (复制)
keyCombo([0x11], 0x56); // Ctrl+V (粘贴)
keyCombo([0x11, 0x10], 0x1B); // Ctrl+Shift+Esc (任务管理器)
keyCombo([0x12], 0x09); // Alt+Tab输入单个 Unicode 字符。
输入一段文字字符串。
keyTypeText("Hello, 世界!");异步获取按键状态(是否正在被按下)。
if (getAsyncKeyState(0x01)) { /* 鼠标左键正在按下 */ }获取按键状态。bit 15 = 是否按下,bit 0 = 是否切换(如 CapsLock)。
获取全部 256 个虚拟键的状态,返回 Buffer(256 字节)。
虚拟键码↔扫描码↔字符映射。mapType:0 = VK→扫描码, 1 = 扫描码→VK, 2 = VK→字符, 3 = 扫描码→VK(区分左右)。
字符→虚拟键码。低字节为 VK 码,高字节为修饰键状态(bit 0=Shift, bit 1=Ctrl, bit 2=Alt)。
通过 ShellExecuteEx 启动程序。返回 ProcessInfo { processId, threadId, handle }。
| 参数 | 说明 |
|---|---|
verb |
操作:"open"(默认)、"runas"(管理员)、"print" 等,传 null 为默认 |
file |
要打开的文件或程序路径 |
args |
命令行参数 |
directory |
工作目录 |
show |
显示方式:1=正常, 0=隐藏, 3=最大化, 6=最小化 |
const proc = shellExecute(null, "notepad.exe", null, null, 1);
console.log(proc.processId, proc.handle);
// 以管理员权限运行
shellExecute("runas", "cmd.exe", null, null, 1);通过 CreateProcess 启动程序,返回 ProcessInfo。
const proc = createProcess("C:\\Windows\\notepad.exe", null, 1);通过进程 ID 终止进程。
terminateProcessById(1234, 0);通过进程句柄终止进程。
关闭句柄(不会终止进程)。
检查进程是否仍在运行。
if (isProcessActive(proc.handle)) {
console.log("进程仍在运行");
}从进程句柄获取 PID。
等待进程退出。timeoutMs 传 0xFFFFFFFF 表示无限等待。
等待进程初始化完成(用于启动后等待窗口就绪)。
枚举所有运行中的进程,返回 ProcessEntry[]。
const procs = enumProcesses();
for (const p of procs) {
console.log(`PID: ${p.processId} 父PID: ${p.parentProcessId} 线程: ${p.threadCount} ${p.exeFile}`);
}按进程名模糊搜索(不区分大小写)。
const notepads = findProcesses("notepad");按名称批量终止进程,返回被终止的 PID 列表。
const killed = killProcessesByName("notepad", 0);
console.log(`已终止 ${killed.length} 个进程`);基于 NCC(归一化互相关)算法 + rayon 多线程两阶段搜索。1920x1080 全屏搜索约 400ms。
在源图像 buffer 中搜索模板,返回第一个匹配(或 null)。
const src = loadImage("screenshot.png");
const tpl = loadImage("target.png");
const match = findImage(src.buffer, src.width, src.height, tpl.buffer, tpl.width, tpl.height, {
threshold: 0.9,
});
if (match) {
console.log(`找到: (${match.x}, ${match.y}) 匹配度: ${(match.score * 100).toFixed(1)}%`);
}查找所有匹配位置,返回 MatchResult[]。
直接在屏幕上搜索模板(自动截图),返回第一个匹配。
const tpl = loadImage("button.png");
const match = findOnScreen(tpl.buffer, tpl.width, tpl.height, {
threshold: 0.85,
});
if (match) {
mouseMoveTo(match.x + match.width / 2, match.y + match.height / 2);
mouseClick(0);
}在屏幕上搜索所有匹配位置。
MatchOptions 参数:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
threshold |
number |
0.8 |
匹配阈值 (0~1),越高越严格 |
maxResults |
number |
10 |
最大返回数量(仅 findAll* 有效) |
step |
number |
自动 | 搜索步长(内部自动两阶段优化) |
regionX |
number |
0 |
搜索区域 X 起点 |
regionY |
number |
0 |
搜索区域 Y 起点 |
regionWidth |
number |
全屏 | 搜索区域宽度 |
regionHeight |
number |
全屏 | 搜索区域高度 |
MatchResult 返回值:
| 字段 | 类型 | 说明 |
|---|---|---|
x |
number |
匹配位置 X(左上角) |
y |
number |
匹配位置 Y(左上角) |
width |
number |
模板宽度 |
height |
number |
模板高度 |
score |
number |
匹配分数 (0~1) |
获取图像 buffer 中指定坐标的颜色,返回 Color { r, g, b, a }。
const screen = captureScreen();
const color = getColorAt(screen.buffer, screen.width, 100, 200);
console.log(`rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`);直接获取屏幕上指定坐标的颜色(自动截图 1x1 区域)。
在图像 buffer 中查找指定颜色,返回 Point { x, y } 或 null。
const screen = captureScreen();
const pt = findColor(screen.buffer, screen.width, screen.height, 255, 0, 0, 30);
if (pt) console.log(`找到红色: (${pt.x}, ${pt.y})`);直接在屏幕上查找指定颜色。tolerance 默认 10,值越大容差越大。
从文件加载图片(支持 PNG、JPEG、BMP 等),返回 LoadedImage { width, height, buffer }。
const img = loadImage("template.png");将 RGBA buffer 保存为图片文件(根据扩展名决定格式)。
const screen = captureScreen();
saveImage("screenshot.png", screen.buffer, screen.width, screen.height);interface ImageData { width: number; height: number; buffer: Buffer }
interface LoadedImage { width: number; height: number; buffer: Buffer }
interface Rect { left: number; top: number; right: number; bottom: number }
interface Point { x: number; y: number }
interface WindowPlacement {
flags: number; showCmd: number;
minPosition: Point; maxPosition: Point; normalPosition: Rect;
}
interface ThreadProcessId { threadId: number; processId: number }
interface CursorInfo { flags: number; showing: boolean; handle: number; x: number; y: number }
interface ProcessInfo { processId: number; threadId: number; handle: number }
interface ProcessEntry { processId: number; parentProcessId: number; threadCount: number; exeFile: string }
interface MatchResult { x: number; y: number; width: number; height: number; score: number }
interface MatchOptions {
threshold?: number; step?: number; maxResults?: number;
regionX?: number; regionY?: number; regionWidth?: number; regionHeight?: number;
}
interface Color { r: number; g: number; b: number; a: number }遇到 Bug、有功能建议或使用疑问,欢迎提交 Issue,会尽快回复。
ISC