# rust-rpa

基于 Rust 的高性能 Node.js RPA（机器人流程自动化）库。

## 特性

- 🚀 **高性能**: 使用 Rust 编写，napi-rs 绑定，追求极致速度和效率
- 🔄 **跨平台**: 支持 Windows 和 macOS
- 📦 **易于使用**: 简洁的面向对象 API，支持 TypeScript
- ⚡ **非阻塞**: 异步操作不会阻塞 Node.js 事件循环
- 🔒 **类型安全**: 完整的 TypeScript 类型定义
- 🖱️ **输入自动化**: Mouse 和 Keyboard 类，完整的输入控制
- 📑 **窗口管理**: 基于 XCap 实现窗口枚举和信息获取
- 👁️ **窗口内容监听**: `Window.watchChanges()` 降采样差异检测，内容变化时再截图，低开销监听 UI 变化
- 🖥️ **多显示器支持**: Monitor 类，支持多显示器设置
- 📸 **屏幕截图**: 基于 XCap 实现全屏和窗口截图，多种图片格式（PNG/JPEG/BMP）
- 🔑 **权限管理**: Permission 类，自动检测并弹出系统权限授权对话框
- 🔍 **OCR 文字识别**: 基于 PP-OCRv5_mobile 模型和 MNN 推理框架，支持中英日文字识别
- 🎯 **模板匹配**: GPU 加速的图标查找功能，支持阈值和区域搜索
- 🖼️ **图像处理**: 裁剪、缩放（Lanczos3）、灰度化等，支持 RGBA 原始数据访问

## 安装

```bash
npm install rust-rpa
```

**支持的平台:**
- Windows (x64, x86)
- macOS (Intel x64, Apple Silicon ARM64)

**注意:** 当前版本不支持 Linux。

## 系统要求

### Windows
- Windows 10 或更高版本 (x64 或 x86)
- 某些操作可能需要管理员权限
- **编译需要**: Visual Studio Build Tools + LLVM (ocr-rs 依赖 bindgen，需要 libclang)

### macOS
- macOS 10.13 或更高版本 (Intel 或 Apple Silicon)
- **需要辅助功能权限**（见下文）

## 命令行工具

安装后可通过 `rpa` 命令直接使用，无需编写代码：

```bash
# 查看帮助
rpa --help

# 检查权限（macOS 自动弹出授权）
rpa permission --prompt

# 列出所有窗口
rpa window list

# 将窗口置于最前
rpa window focus Chrome

# 设置窗口位置和大小
rpa window bounds Chrome 100 100 800 600

# 截取主显示器
rpa screenshot -o desktop.png

# 截取指定窗口
rpa screenshot --window Chrome -o chrome.png

# 鼠标操作
rpa mouse position
rpa mouse move 500 300
rpa mouse move-rel 100 50
rpa mouse click right
rpa mouse scroll 0 -3

# 键盘操作
rpa keyboard type "Hello World"
rpa keyboard press enter
rpa keyboard press ctrl+c

# 剪贴板
rpa clipboard read
rpa clipboard write "Hello"
rpa clipboard paste
rpa clipboard paste-text "直接粘贴的文本"
rpa clipboard paste-image screenshot.png

# 列出所有显示器
rpa monitor list

# OCR 文字识别
rpa monitor recognize-text
rpa monitor find-text "确定"
rpa window find-text Chrome "搜索"

# 图片处理
rpa image info screenshot.png
rpa image crop screenshot.png 0 0 800 600 -o cropped.png
rpa image find-text screenshot.png "文字"

# 快捷命令（顶层）
rpa click right
rpa move 500 300
rpa type "Hello"
rpa press enter
rpa paste
rpa pause 1000
```

所有命令支持 `--help` 查看详细用法，如 `rpa screenshot --help`。

## 示例代码

项目提供了丰富的示例代码，位于 `examples/` 目录中：

- `permission-demo.js` - 权限检查演示
- `list-windows.js` - 窗口管理演示
- `screenshot-demo.js` - 屏幕截图功能演示
- `clipboard-demo.js` - 剪贴板操作演示
- `image-processing-demo.js` - 图像处理功能演示
- `find-icon-demo.js` - 完整的 findIcon 功能演示
- `find-icon-basic.js` - findIcon 基础用法示例
- `window-watch-changes-demo.js` - 监听窗口内容变化（`watchChanges`），可选 `chrome` / `ding` / `qianniu`

运行示例：
```bash
node examples/permission-demo.js
node examples/list-windows.js
node examples/window-watch-changes-demo.js chrome
```

## 使用方法

### JavaScript

```javascript
const { Window, Monitor, Mouse, Keyboard, Permission } = require('rust-rpa');

async function main() {
  try {
    // 检查权限（macOS 上无权限会自动弹出授权对话框）
    Permission.checkAccessibility();
    Permission.checkScreenCapture();

    // 获取所有窗口
    const windows = Window.all();
    console.log(`Found ${windows.length} windows`);
    
    windows.forEach(win => {
      console.log(`${win.appName()}: ${win.title()}`);
      console.log(`  PID: ${win.pid()}, Parent PID: ${win.parentId()}`);
      console.log(`  Position: (${win.x()}, ${win.y()}), Size: ${win.width()}x${win.height()}`);
    });
    
    // 将窗口置于最前
    await windows[0].bringToFront();
    
    // 获取所有显示器
    const monitors = Monitor.all();
    const primaryMonitor = monitors.find(m => m.isPrimary());
    
    // 截图
    const image = await primaryMonitor.captureImage();
    const pngBuffer = await image.toPng();
    
    // 鼠标和键盘控制
    await Mouse.moveTo(100, 100);
    await Mouse.click('left');
    await Keyboard.typeText('Hello World!');
  } catch (error) {
    console.error('Error:', error.message);
  }
}

main();
```

## API 文档

完整的 TypeScript 类型定义请参考 `index.d.ts` 文件。

### Window 类

窗口管理类。

**静态方法：**
- `Window.all(): Window[]` - 获取所有可见窗口

**实例方法：**
- `id(): number` - 窗口 ID
- `pid(): number` - 进程 ID
- `parentId(): number` - 父进程 ID
- `title(): string` - 窗口标题
- `appName(): string` - 应用程序名称
- `x(), y(), width(), height()` - 位置和尺寸
- `getSize(): { width, height }` - 窗口尺寸
- `isMinimized(), isMaximized(), isFocused()` - 窗口状态
- `bringToFront()` - 置顶窗口
- `maximize(), minimize()` - 最大化/最小化
- `currentMonitor(): Monitor` - 获取所在显示器
- `currentMonitorId(): number` - 显示器 ID
- `getBounds(): { x, y, width, height }` - 窗口边界
- `setBounds(bounds)` - 设置窗口边界
- `toJSON()` - 转为 JSON 对象
- `captureImage(options?)` - 截图
- `watchChanges(options?, callback)` - 监听窗口内容变化；返回 `WindowWatcher`（`stop()` / `isRunning()` / `changeOptions()`）；启动时先建基线**不**调回调，仅在检测到相对上一帧的变化时调用 `callback(info)`（`info` 含 `changeRatio` 变化比例和 `changedPixels` 估算变化像素数）；`regions` 可指定多个逻辑像素监控区域；`minChangedPixels` 可按原图逻辑像素数控制触发门槛；`minPixelDiff` 可设置单像素最小通道差异（默认 30）过滤截图噪声
- `recognizeText(options?)` - OCR 识别文字
- `findText(text, options?)` - 查找文字
- `findIcon(template, options?)` - 查找图标
- `waitText(text, options?)` - 等待文字出现
- `waitIcon(template, options?)` - 等待图标出现
- `clickText(text, options?)` - 点击文字
- `clickIcon(template, options?)` - 点击图标

**Window 专属 Options：**
- `from?: 'window' | 'screen'` - 截图来源（`'window'` 默认截窗口，`'screen'` 截整屏）

### Monitor 类

显示器管理类。

**静态方法：**
- `Monitor.all(): Monitor[]` - 获取所有显示器

**实例方法：**
- `id(): number` - 显示器 ID
- `name(): string` - 显示器名称
- `x(), y(), width(), height()` - 位置和尺寸
- `scaleFactor(): number` - 缩放因子
- `isPrimary(): boolean` - 是否主显示器
- `captureImage(options?)` - 截图
- `recognizeText(options?)` - OCR 识别文字
- `findText(text, options?)` - 查找文字
- `findIcon(template, options?)` - 查找图标
- `waitText(text, options?)` - 等待文字出现
- `waitIcon(template, options?)` - 等待图标出现
- `clickText(text, options?)` - 点击文字
- `clickIcon(template, options?)` - 点击图标

### ImageData 类

图像处理类。

**静态方法：**
- `ImageData.fromFile(filePath)` - 从文件加载
- `ImageData.fromBuffer(buffer)` - 从 Buffer 解码

**属性：**
- `width: number` - 图像宽度
- `height: number` - 图像高度

**方法：**
- `toPng(), toJpeg(), toBmp()` - 格式转换
- `toFile(filePath)` - 保存到文件
- `crop(x, y, width, height)` - 裁剪
- `resize(width, height)` - 缩放
- `grayscale()` - 灰度化
- `getRawData(): Buffer` - 获取原始像素数据
- `metadata()` - 获取图片元信息
- `recognizeText(options?)` - OCR 识别文字
- `findText(text, options?)` - 查找文字
- `findIcon(template, options?)` - 模板匹配

### Mouse 类

鼠标控制类（静态方法）。

- `Mouse.moveTo(x, y, duration?)` - 移动到坐标（duration 动画时间，秒）
- `Mouse.moveRel(dx, dy, duration?)` - 相对移动
- `Mouse.click(button?)` - 点击（`'left'`, `'right'`, `'middle'`）
- `Mouse.doubleClick(button?)` - 双击
- `Mouse.down(button?), Mouse.up(button?)` - 按下/释放
- `Mouse.scroll(deltaX?, deltaY?)` - 滚动（Y 正数向上、负数向下）
- `Mouse.position()` - 获取当前位置

### Keyboard 类

键盘控制类（静态方法）。

- `Keyboard.typeText(text)` - 输入文本
- `Keyboard.click(key)` - 按下并释放按键
- `Keyboard.down(key), Keyboard.up(key)` - 按下/释放
- `Keyboard.sequence(keys)` - 按键序列（如 `[Key.Ctrl, Key.C]`）

**Key 枚举：**`Key.Enter`, `Key.Ctrl`, `Key.F1`, `Key.A` 等

### Clipboard 类

剪贴板控制类（静态方法）。

- `Clipboard.readText()` - 读取剪贴板文本
- `Clipboard.writeText(text)` - 写入文本
- `Clipboard.writeImage(source)` - 写入图片（支持文件路径、base64、Buffer）
- `Clipboard.writeFile(paths)` - 写入文件路径
- `Clipboard.paste()` - 执行粘贴
- `Clipboard.pasteText(text)` - 写入并粘贴（自动恢复剪贴板）
- `Clipboard.pasteImage(source)` - 写入图片并粘贴（自动恢复）
- `Clipboard.pasteFile(paths)` - 写入文件路径并粘贴

### Permission 类

权限检查类（静态方法）。

- `Permission.checkAccessibility(prompt?)` - 检查辅助功能权限
- `Permission.checkScreenCapture(prompt?)` - 检查屏幕录制权限

**说明：**`prompt` 默认为 `true`，无权限时自动弹出系统授权对话框（仅 macOS）

### pause 函数

- `pause(ms)` - 等待指定毫秒数

```javascript
const { pause } = require('rust-rpa');
await pause(1000);  // 等待 1 秒
```

## macOS 权限要求

### 辅助功能权限（输入控制）

在 macOS 上使用鼠标和键盘控制功能时，需要授予辅助功能权限：

1. 打开 **系统偏好设置**
2. 进入 **安全性与隐私** > **隐私** > **辅助功能**
3. 点击锁图标进行更改
4. 将您的终端应用程序或 IDE 添加到列表中
5. 重启应用程序

### 屏幕录制权限（截图功能）

使用截图功能时，需要授予屏幕录制权限：

1. 打开 **系统偏好设置**
2. 进入 **安全性与隐私** > **隐私** > **屏幕录制**
3. 点击锁图标进行更改
4. 勾选您的终端应用程序或 IDE
5. 重启应用程序

## 示例

### 窗口管理

```javascript
const { Window } = require('rust-rpa');

// 获取所有窗口
const windows = Window.all();
console.log(`Found ${windows.length} windows`);

// 遍历窗口
windows.forEach(win => {
  console.log(`${win.appName()}: ${win.title()}`);
  console.log(`  PID: ${win.pid()}, Parent PID: ${win.parentId()}`);
  console.log(`  Position: (${win.x()}, ${win.y()})`);
  console.log(`  Size: ${win.width()}x${win.height()}`);
  console.log(`  Minimized: ${win.isMinimized()}, Focused: ${win.isFocused()}`);
});

// 将窗口置于最前
const target = windows.find(w => w.appName().includes('Chrome'));
if (target) {
  await target.bringToFront();
}

// 获取/设置窗口边界
const bounds = windows[0].getBounds();
console.log(`Bounds: ${JSON.stringify(bounds)}`);
await windows[0].setBounds({ x: 100, y: 100, width: 800, height: 600 });

// 获取窗口所在显示器
const monitor = windows[0].currentMonitor();
console.log(`Window is on monitor: ${monitor.name()}`);

// 转为 JSON 对象
console.log(JSON.stringify(windows[0].toJSON(), null, 2));
```

### 显示器管理

```javascript
const { Monitor } = require('rust-rpa');

// 获取所有显示器
const monitors = Monitor.all();
monitors.forEach(m => {
  console.log(`Monitor: ${m.name()}`);
  console.log(`  Resolution: ${m.width()}x${m.height()}`);
  console.log(`  Scale: ${m.scaleFactor()}x`);
  console.log(`  Primary: ${m.isPrimary()}`);
});

// 获取主显示器
const primary = monitors.find(m => m.isPrimary());
```

### 屏幕截图

```javascript
const fs = require('fs');
const { Monitor, Window, ImageData } = require('rust-rpa');

// 截取显示器
const monitors = Monitor.all();
const image = await monitors[0].captureImage();

// 截取时指定目标尺寸（自动缩放，便于后续扩展其他选项）
const imageResized = await monitors[0].captureImage({ size: { width: 800, height: 600 } });

// 保存为不同格式（方式 1：转换为 Buffer）
const pngBuffer = await image.toPng();
fs.writeFileSync('screenshot.png', pngBuffer);

const jpegBuffer = await image.toJpeg();
fs.writeFileSync('screenshot.jpg', jpegBuffer);

// 保存为不同格式（方式 2：直接保存文件）
await image.toFile('screenshot.png');
await image.toFile('screenshot.jpg');
await image.toFile('screenshot.bmp');

// 裁剪图像
const cropped = await image.crop(0, 0, 800, 600);
await cropped.toFile('cropped.png');

// 截取窗口
const windows = Window.all();
if (windows.length > 0) {
  const windowImage = await windows[0].captureImage();
  await windowImage.toFile('window.png');
}
```

**截图选项**：`captureImage(options?)` 接受可选对象 `CaptureImageOptions`：
- `size?: { width, height }`：截取后缩放到指定宽高，不传则返回原始尺寸。
- **Window 专用**：`from?: 'window' | 'screen'` 默认截窗口，设为 `'screen'` 可截窗口所在显示器整屏再配合 `getBounds()` 与 `image.crop()` 裁剪；`region?: { x, y, width, height }` 仅截取该逻辑像素区域；`autoSize?: true` 在 Windows 高 DPI 下自动将图像缩放到逻辑像素宽高（与 `getSize`/`getBounds` 一致）。

### 输入自动化

```javascript
const { Mouse, MouseButton, Keyboard, Key } = require('rust-rpa');

// 鼠标操作
await Mouse.moveTo(100, 200);              // 瞬间移动鼠标
await Mouse.moveTo(100, 200, 0.5);         // 平滑动画移动，持续 0.5 秒
await Mouse.moveRel(50, 30);               // 相对移动（从当前位置向右 50、向下 30）
await Mouse.click(MouseButton.Left);       // 左键点击
await Mouse.click(MouseButton.Right);      // 右键点击
await Mouse.doubleClick();                 // 双击
await Mouse.scroll(0, 3);          // 向上滚动（正值 = 远离用户）
await Mouse.scroll(0, -3);         // 向下滚动（负值 = 朝向用户）

// 获取鼠标位置
const { x, y } = await Mouse.position();
console.log(`Mouse at (${x}, ${y})`);

// 键盘操作（key 可用 Key 枚举或字符串）
await Keyboard.typeText('Hello World!');  // 输入文本
await Keyboard.click(Key.Enter);          // 按 Enter

// 组合键
await Keyboard.sequence([Key.Ctrl, Key.A]);  // Ctrl+A (全选)
await Keyboard.sequence([Key.Ctrl, Key.C]);  // Ctrl+C (复制)
await Keyboard.sequence([Key.Meta, Key.V]);  // Cmd+V (粘贴，macOS)

// 按键按下和释放
await Keyboard.down(Key.Shift);
await Keyboard.click(Key.A);
await Keyboard.up(Key.Shift);
```

### 剪贴板操作

```javascript
const path = require('path');
const { Clipboard, ImageData } = require('rust-rpa');

// 读取剪贴板
const text = await Clipboard.readText();
console.log('剪贴板内容:', text);

// 写入剪贴板
await Clipboard.writeText('Hello, Clipboard!');

// 写入图片到剪贴板（支持文件路径、Buffer、base64）
await Clipboard.writeImage(path.join(__dirname, 'test/images/ding_copy.png'));

// 写入文件到剪贴板（粘贴时为文件）
await Clipboard.writeFile(path.join(__dirname, 'rust-rpa.darwin-x64.node'));

// 执行粘贴操作（使用 Cmd/Ctrl+V）
await Clipboard.paste();

// pasteText / pasteImage 会自动恢复剪贴板；pasteFile 不会自动恢复
await Clipboard.pasteText('直接粘贴文本');
await Clipboard.pasteImage('image.png');
await Clipboard.pasteFile('/path/to/file.pdf');
```

### OCR 文字识别

利用 PP-OCRv5_mobile 模型，支持在 **ImageData**、**Window** 和 **Monitor** 上直接执行 OCR。

```javascript
const { Monitor, Window, ImageData } = require('rust-rpa');

// --- ImageData 正中 OCR ---
const image = await ImageData.fromFile('screenshot.png');

// 识别图片中所有文字
const results = await image.recognizeText();
for (const result of results) {
  console.log(`文字: ${result.text}`);
  console.log(`  位置: (${result.x}, ${result.y}), 大小: ${result.width}x${result.height}`);
  console.log(`  置信度: ${(result.confidence * 100).toFixed(1)}%`);
}

// 查找指定文字
const found = await image.findText('确定');
if (found) {
  console.log(`找到"确定"在: (${found.x}, ${found.y})`);
}

// --- Monitor 上 OCR ---
const monitors = Monitor.all();
const monitorResults = await monitors[0].recognizeText();
console.log(`显示器上数到 ${monitorResults.length} 条文字`);

// 在显示器上查找文字
const monitorFound = await monitors[0].findText('文件');
if (monitorFound) {
  console.log(`在显示器上找到"文件": (${monitorFound.x}, ${monitorFound.y})`);
}

// --- Window 上 OCR ---
const windows = Window.all();
const targetWindow = windows.find(w => w.appName().includes('Chrome'));
if (targetWindow) {
  // 识别窗口中的文字（内部先截图）
  const windowResults = await targetWindow.recognizeText();
  console.log(`Chrome 窗口中有 ${windowResults.length} 条文字`);
  
  // 在窗口中查找指定文字
  const windowFound = await targetWindow.findText('搜索');
  if (windowFound) {
    console.log(`在窗口中找到"搜索": (${windowFound.x}, ${windowFound.y})`);
  }
  
  // 使用 notText 排除同一行的干扰文字
  // 场景：同一行有"确定"和"取消"两个按钮，需要点击"确定"但排除"取消"所在行
  const confirmBtn = await targetWindow.findText('确定', { notText: '取消' });
  if (confirmBtn) {
    console.log(`找到不与"取消"同行的"确定": (${confirmBtn.x}, ${confirmBtn.y})`);
  }
  
  // clickText 也支持 notText 参数
  const clickResult = await targetWindow.clickText('发送', { notText: '草稿', timeout: 5000 });
  console.log(`点击位置: (${clickResult.x}, ${clickResult.y})`);
  
  // 使用 from 参数指定截图来源（'window' 默认截窗口，'screen' 截窗口所在显示器整屏）
  // 场景：窗口被其他窗口遮挡时，从屏幕截图可以获取更完整的内容
  const screenResult = await targetWindow.findText('提示', { from: 'screen' });
  if (screenResult) {
    console.log(`在屏幕截图中找到"提示": (${screenResult.x}, ${screenResult.y})`);
  }
  
  // recognizeText 和 waitText 同样支持 from 参数
  const screenTexts = await targetWindow.recognizeText({ from: 'screen' });
  console.log(`屏幕截图中识别到 ${screenTexts.length} 条文字`);
  
  const waitResult = await targetWindow.waitText('加载完成', { from: 'screen', timeout: 10000 });
  console.log(`等待到文字出现: (${waitResult.x}, ${waitResult.y})`);
}
```

## 平台支持

| 平台 | 架构 | 状态 | 备注 |
|----------|-------------|--------|-------|
| Windows | x64 | ✅ 支持 | 可能需要管理员权限 |
| Windows | x86 (32位) | ✅ 支持 | 可能需要管理员权限 |
| macOS | x64 (Intel) | ✅ 支持 | 需要辅助功能权限 |
| macOS | ARM64 (Apple Silicon) | ✅ 支持 | 需要辅助功能权限 |
| Linux | 全部 | ❌ 不支持 | 当前版本无计划 |

## 开发

从源码构建和开发指南请参阅 [dev.md](./dev.md)。

## 更新日志

### 0.2.7

#### 新功能

- **Window.watchChanges 新增 `minChangedPixels` 参数**: 支持按原图逻辑像素数控制变化触发，与 `threshold` 为 AND 关系
  - `minChangedPixels: 0`（默认）- 不使用此条件，仅靠 `threshold` 控制
  - `minChangedPixels > 0` - 估算的原图逻辑像素变化数须 >= 该值才触发回调
  - 示例：`{ threshold: 0.001, minChangedPixels: 500 }` 表示变化比例超过 0.1% **且** 至少 500 个逻辑像素变化才触发
- **Window.watchChanges 新增 `minPixelDiff` 参数**: 单像素最小通道差异（0-255），默认 30，用于过滤截图捕获噪声
  - `minPixelDiff: 30`（默认）- R/G/B 任一通道差值 ≥ 30 才算“变化”，有效过滤截图噪声（通常差值 1-10）
  - `minPixelDiff: 0` - 任意微小差异都计入（最敏感，但噪声也会触发）
  - `minPixelDiff: 1` - 任意像素差异都计入（精确字节比较）
  - 示例：`{ threshold: 0, minChangedPixels: 2, minPixelDiff: 30 }` 过滤噪声同时保持对光标闪烁等微小变化的灵敏度
- **Window.watchChanges 回调参数改为对象**: `callback(info)`，`info` 为 `{ changeRatio, changedPixels }`，其中 `changeRatio` 为相邻两帧像素变化比例（约 0.0–1.0），`changedPixels` 为估算的原图逻辑像素变化数；**启动首帧不触发回调**（仅建基线）；两帧缓冲区长度不一致时 `changeRatio` 为 `1`、`changedPixels` 为 `4294967295`。

### 0.2.6

#### 行为调整

- **Window.watchChanges 启动**：首帧仅建立对比基线，**不**调用 `callback`；仅在后续轮询检测到相对上一帧的变化时才触发回调。

#### 破坏性变更

- **Window.watchChanges 选项**: `region` 已改为 `regions`（`CaptureRegion` 数组），可指定多个监控区域；
- **`WindowWatcher.changeOptions(options)`**: 可按字段合并更新当前监听参数（仅传入字段覆盖，未传字段保持原值），下一轮轮询起生效；若已 `stop()` 再调用会报错。
- **`Window.watchChanges` 回调参数**: `callback(changeRatio)`，`changeRatio` 为相邻两帧在降采样图上的像素变化比例（约 0.0–1.0）；**启动首帧不触发回调**（仅建基线）；两帧缓冲区长度不一致时为 `1`。

### 0.2.5

#### 改进

- **Window.watchChanges 优化**: `threshold` 参数默认值改为 `0.0`，并优化为 `0` 时跳过图像对比
  - `threshold: 0`（默认）- 跳过像素对比，每次轮询直接触发回调，最低延迟
  - `threshold > 0` - 执行降采样对比，变化比例超过阈值才触发回调
  - 适用于已知窗口会变化、需要立即获取截图的场景

### 0.2.5

#### 新功能

- **findText / waitText / clickText 新增 `index` 参数**: 支持选择第几个匹配的文字结果
  - `index` 默认为 `0`，返回第一个匹配结果
  - 正数表示第 n 个（从 0 开始）：`index: 1` 返回第二个
  - 负数表示倒数第 n 个：`index: -1` 返回最后一个，`index: -2` 返回倒数第二个
  - 超出匹配数量范围时返回 `null`
  - 适用场景：页面中存在多个相同文字，需要点击特定位置
  - 示例：`findText('确定', { index: 1 })` 查找第二个「确定」，`clickText('确定', { index: -2 })` 点击倒数第二个「确定」

#### 类型定义更新

- `FindTextOptions` 新增 `index?: number` 字段
- `WindowFindTextOptions` 新增 `index?: number` 字段
- `MonitorFindTextOptions` 新增 `index?: number` 字段
- `WindowWaitOptions` 新增 `index?: number` 字段
- `MonitorWaitOptions` 新增 `index?: number` 字段
- `WaitOptions` 新增 `index?: number` 字段

### 0.2.4

#### 新功能

- **Window.watchChanges（窗口内容变化监听）**: 后台线程降采样对比相邻帧，超过阈值才触发回调；支持 `threshold`、`from`（`window`/`screen`）、`regions`（多区域，逻辑像素）；返回 `WindowWatcher`，可 `stop()`、`changeOptions()` 运行时改参。

### 0.2.3

#### 修复

- **OCR `recognizeText` 多区域识别修复**: 修复了 `recognizeText` 在指定多个 `regions` 时坐标转换错误的问题

### 0.2.2

#### 新功能

- **Window 文字和图标操作新增 `from` 参数**: `findText`、`waitText`、`clickText`、`recognizeText`、`findIcon`、`waitIcon`、`clickIcon` 方法新增 `from` 选项，用于指定截图来源
  - `from: 'window'`（默认）- 截取当前窗口内容
  - `from: 'screen'` - 截取窗口所在显示器的整屏内容
  - 适用场景：窗口被其他窗口遮挡时，从屏幕截图可以获取更完整的内容
  - 示例：`window.findText('提示', { from: 'screen' })`、`window.waitIcon(template, { from: 'screen' })`

#### 类型定义更新

- **Options 类型分离**: 为 `Window` 和 `Monitor` 类分别定义独立的 Options 类型，使 API 更清晰
  - `WindowMatchOptions` / `MonitorMatchOptions` / `MatchOptionsJs` - 分别用于 `findIcon`
  - `WindowFindTextOptions` / `MonitorFindTextOptions` / `FindTextOptions` - 分别用于 `findText`
  - `WindowRecognizeTextOptions` / `MonitorRecognizeTextOptions` / `RecognizeTextOptions` - 分别用于 `recognizeText`
  - `WindowWaitOptions` / `MonitorWaitOptions` / `WaitOptions` - 分别用于 `waitText` / `waitIcon` / `clickText` / `clickIcon`
  - 只有 Window 专用类型包含 `from` 参数，Monitor 类型不包含（因为 Monitor 本身就是屏幕）

#### 其他改进

- **findText|waitText|clickText** 匹配文字时，模糊匹配，例如 o 和 0 避免 OCR 识别不一致造成无法查找或者点击
- **Window.maximize()**: 最大化窗口
- **Window.minimize()**: 最小化窗口


### 0.2.1

#### 新功能

- **findText / waitText / clickText 增强**: 新增 `notText` 参数，用于排除同一行中包含特定文字的匹配结果
  - `findText(text, { notText: '排除文字' })` - 查找文字时，如果同一行中包含 `notText` 指定的文字，则排除该匹配结果
  - `waitText(text, { notText: '排除文字' })` - 等待文字时同样支持排除逻辑
  - `clickText(text, { notText: '排除文字' })` - 点击文字时同样支持排除逻辑
  - 适用场景：同一行内可能有多个相似文字，需要精确排除干扰项
  - 同一行判断标准：两个文字区域的垂直方向有超过较小区域高度 50% 的重叠

- **clickText / clickIcon 返回值增强**: 返回点击坐标信息 `ClickResult`
  - 返回 `{ x, y, relX, relY }`，包含屏幕坐标和相对坐标
  - 便于调试和验证点击位置

- **ImageData.recognizeText 增强**: 新增 `options.regions` 参数，可指定识别区域列表

#### 类型定义更新

- `FindTextOptions` 新增 `notText?: string` 字段
- `WaitOptions` 新增 `notText?: string` 字段
- `ClickResult` 类型：`{ x: number, y: number, relX: number, relY: number }`

### 0.2.0

#### 新功能

- **OCR 文字识别**: 新增 `recognizeText()` 和 `findText()` 方法
  - 基于 PP-OCRv5_mobile 模型（检测模型 4.7M + 识别模型 16M）
  - 支持中文、英文、日文识别
  - `ImageData.recognizeText()` - 识别图片中的所有文字，返回文字列表及其位置信息
  - `ImageData.findText(text, options?)` - 在图片中查找指定文字，返回匹配文字的位置信息；`options.regions` 可指定查找区域列表以提升性能
- **Window 类增强**: 新增 `findIcon()`、`recognizeText()`、`findText()`、`waitText()`、`waitIcon()`、`clickText()`、`clickIcon()` 方法
  - 所有方法内部先执行 `captureImage({ autoSize: true })`，然后在截图上执行对应操作
  - `Window.findIcon(template, options?)` - 在窗口截图中查找图标
  - `Window.recognizeText()` - 识别窗口中的文字
  - `Window.findText(text, options?)` - 在窗口中查找指定文字
  - `Window.waitText(text, options?)` - 等待指定文字出现，超时抛出错误
  - `Window.waitIcon(template, options?)` - 等待指定图标出现，超时抛出错误
  - `Window.clickText(text, options?)` - 点击指定文字
  - `Window.clickIcon(template, options?)` - 点击指定图标
- **Monitor 类增强**: 新增 `findIcon()`、`recognizeText()`、`findText()`、`waitText()`、`waitIcon()`、`clickText()`、`clickIcon()` 方法
  - 所有方法内部先执行 `captureImage({ autoSize: true })`，然后在截图上执行对应操作
  - `Monitor.findIcon(template, options?)` - 在显示器截图中查找图标
  - `Monitor.recognizeText()` - 识别显示器截图中的文字
  - `Monitor.findText(text, options?)` - 在显示器截图中查找指定文字
  - `Monitor.waitText(text, options?)` - 等待指定文字出现，超时抛出错误
  - `Monitor.waitIcon(template, options?)` - 等待指定图标出现，超时抛出错误
  - `Monitor.clickText(text, options?)` - 点击指定文字
  - `Monitor.clickIcon(template, options?)` - 点击指定图标
- **Mouse 类增强**: 新增 `moveRel()` 方法
  - `Mouse.moveRel(dx, dy, duration?)` - 基于当前鼠标位置进行相对移动
  - 支持可选的 `duration` 参数实现平滑动画移动
- **CLI 命令增强**: 新增多个便捷命令
  - `rpa mouse move-rel <dx> <dy>` - 相对移动鼠标
  - `rpa clipboard paste-text` / `rpa clipboard paste-image` - 粘贴文本/图片并自动恢复剪贴板
  - `rpa image find-text` / `rpa image recognize-text` - 图片 OCR 操作
  - 顶层快捷命令: `rpa click`, `rpa move`, `rpa type`, `rpa press`, `rpa paste`, `rpa pause`
- **npm 包优化**: 子模块迁移至 `@alibot` scoped package
  - 更清晰的包命名空间管理
  - 自动同步子模块版本与主包版本

#### BREAKCHANGE

- **`captureImage` 的 `autoSize` 默认值改为 `true`**: `Monitor.captureImage()` 和 `Window.captureImage()` 的 `autoSize` 选项默认值从 `false` 改为 `true`
  - 现在默认自动将图像缩放到逻辑像素尺寸，与 `getBounds()`/`getSize()` 返回的坐标一致
  - 如需物理像素图像，需显式传入 `autoSize: false`
  - 影响范围：所有依赖截图尺寸的逻辑（OCR、模板匹配等）现在默认使用逻辑坐标
  - rpa部分命令简化

#### API 变更

- **`findIcon` 返回值调整**: `findIcon()` 方法在未找到匹配时现在返回 `null` 而非 `{ found: false, ... }` 对象
  - 找到匹配: 返回 `MatchResult` 对象 `{ found: true, score, x, y, width, height }`
  - 未找到匹配: 返回 `null`
  - 影响范围: `ImageData.findIcon()`, `Window.findIcon()`, `Monitor.findIcon()`
  - 迁移指南: 使用 `if (result) { ... }` 或 `if (result !== null) { ... }` 检查返回值
  ```javascript
  const result = await image.findIcon(template);
  if (result) {
    console.log(`找到图标: (${result.x}, ${result.y}), 置信度: ${result.score}`);
  } else {
    console.log('未找到图标');
  }
  ```

### 0.1.7

#### BREAKCHANGE

- **`Mouse.scroll` 滚动单位改为像素**: `Mouse.scroll(dx, dy)` 的 `dx` 和 `dy` 参数现在表示像素值，而非滚动行数
  - 此前 Windows 和 macOS 使用不同的滚动单位，行为不一致
  - 现在统一按像素处理：传入 200 表示尝试滚动约 200 像素
  - Windows: 30 像素 ≈ 1 个 WHEEL_DELTA (120)
  - macOS: 20 像素 ≈ 1 行滚动
- **Windows 平台 `Mouse.scroll` 方向调整**: `dy` 正数现在表示向下滚动（与 macOS 保持一致）
  - 此前 Windows 上正数向上滚动，与 macOS 相反
  - 现在双平台方向一致：正数向下，负数向上

#### 功能

- **所有输入操作自动添加延迟**: `Mouse` 和 `Keyboard` 类的所有操作方法现在自动在返回前添加 50ms 延迟
  - 包括：`moveTo`, `click`, `doubleClick`, `down`, `up`, `scroll`
  - 包括：`Keyboard.click`, `typeText`, `down`, `up`, `sequence`
  - 确保系统有足够时间处理操作，避免后续操作立即执行导致失败
- **剪贴板操作自动添加延迟**: `Clipboard` 类的写入和粘贴方法现在自动添加 50ms 延迟
  - 包括：`writeText`, `writeImage`, `writeFile`, `paste`
  - 确保剪贴板内容写入完成后再执行后续操作
- **窗口操作自动添加延迟**: `Window` 类的状态修改方法现在自动添加 50ms 延迟
  - 包括：`bringToFront`, `setBounds`
  - 确保窗口状态变更完成后再执行后续操作
- **`clickText`/`clickIcon` 返回点击坐标**: `Window.clickText()` 和 `Window.clickIcon()` 现在返回 `ClickResult` 对象
  - 返回 `{ x, y }` 表示实际点击的屏幕坐标
  - 便于调试和验证点击位置

### 0.1.6

#### BREAKCHANGE

- **Windows 平台鼠标坐标统一为逻辑像素**: `Mouse.moveTo()` 和 `Mouse.position()` 现在统一使用逻辑像素坐标，与 `Window.getBounds()` 等 API 保持一致
  - 此前 Windows 平台上鼠标操作使用物理像素，在高 DPI 缩放环境下与窗口坐标不一致
  - 现在 Windows 平台会自动处理 DPI 缩放，传入和返回的坐标均为逻辑像素
  - macOS 平台不受影响（原本就使用逻辑像素）


### 0.1.5

#### 功能
- **`Mouse.moveTo` 增强**: 新增可选 `duration` 参数，支持平滑动画移动
  - `duration` 为移动动画持续时间（秒），默认为 0（瞬间移动）
  - 当 `duration > 0` 时，鼠标以平滑动画方式移动到目标点，使用 easeInOutCubic 缓动函数
  - 动画过程中会触发 mouseMove 事件，网页等应用可正常响应鼠标移动
- **`findIcon` 参数优化**: `options` 参数中的 `threshold` 和 `regions` 字段均改为可选
  - `threshold` 不传时默认使用 0.8
  - `regions` 中的区域可超出图片边界，会自动裁剪为与图片的交集范围进行查找
- **Window.captureImage 选项 `from`**：新增 `'window'`（默认）与 `'screen'`。设为 `'screen'` 时先截取窗口所在显示器整屏，再自动裁剪为当前窗口与显示器的交集区域（超出显示器的部分忽略）。
- **Window.captureImage 选项 `region`**：新增可选参数，仅截取指定逻辑像素区域 `{ x, y, width, height }`，不填则截取全图。
- **Window.captureImage 选项 `auto`**：新增可选参数。为 `true` 时，在 Windows 存在 DPI 缩放的情况下自动将图像缩放到逻辑像素宽高（与 `getSize`/`getBounds` 一致）。
- **CaptureImageOptions 处理顺序**：内部先执行 `autoSize`（缩放到逻辑像素），再执行 `region`（按逻辑像素裁剪），最后应用 `size`（若有）。启用 `autoSize` 后，`region` 与逻辑像素坐标一一对应。

### 0.1.4

#### BREAKCHANGE
- `toJson()`改为`toJSON()`
- `window` `toJSON()` 返回对象中取消`bounds`
- **Window 数据一致性修复**: 统一 `getBounds()` 和 `toJSON()`、`setBounds()` 中屏幕宽高和窗口大小位置都使用逻辑像素，解决 Windows 环境下坐标不一致问题。

### 0.1.3
#### 功能
- **新增 `Window.getSize()` 方法**: 返回 `{width, height}` 对象，提供便捷的窗口尺寸获取方式
- **新增 `Window.currentMonitorId()` 方法**: 获取窗口所在显示器的 ID，比 `currentMonitor()` 更轻量
- **`Window.toJSON()` 增强**: 现在包含 `monitorId` 字段，提供窗口所在显示器的 ID 信息

#### 修复
- `bringToFront`在mac中仅将当前窗口顶置，避免顶置其父窗口
- windows缩放后，应用程序大小和屏幕大小使用的逻辑像素逻辑不一致，都改为使用真实逻辑像素，减少后续处理

### 0.1.2

#### 修复
- `windows`上的`.node`文件调用失败修复

### 0.1.1

#### 新增

- **CLI 命令行工具**：安装后可通过 `rpa` 命令直接使用，支持窗口/鼠标/键盘/剪贴板/截图/显示器/权限等全部功能
- **Permission 类**：新增 `checkAccessibility()` 和 `checkScreenCapture()`，macOS 自动弹出系统授权对话框，Windows 检查管理员权限
- **Window.parentId()**：获取窗口所属进程的父进程 ID
- **Window.bringToFront()**：将窗口置于最前，改为异步方法返回 `Promise<void>`
- **Window.setBounds()**：设置窗口位置和大小，改为异步方法返回 `Promise<void>`
- **Clipboard.pasteText(text)**：写入文本并粘贴，完成后自动恢复剪贴板原始内容
- **Clipboard.pasteImage(source)**：写入图片并粘贴，完成后自动恢复剪贴板原始内容
- **Clipboard.pasteFile(paths)**：写入文件并粘贴，不会自动恢复剪贴板原始内容
- **ImageData.fromBase64(base64)**：从 base64 字符串或 data URI 创建 ImageData 对象

#### BreakChange

- **Clipboard.writeImage(source)**：简化接口，不再需要手动传 `width`/`height`，支持文件路径、Buffer、base64 字符串、data URI 四种输入方式
- **bringWindowToFrontById**：已移除公开暴露，改为 `bringToFront()` 实例方法内部调用
- **WindowToJson / MonitorToJson**：修正 TypeScript 定义中的字段命名为 camelCase（与 napi-rs 实际输出一致）
- **Node.js 版本要求**：最低版本从 14 提升至 18

## 许可证

MIT

## 致谢

构建基于以下项目:
- [Rust](https://www.rust-lang.org/) - Rust 编程语言
- [napi-rs](https://napi.rs/) - Node.js 的 Rust 绑定
- [XCap](https://github.com/nashaofu/xcap) - 跨平台截图库
- [windows-rs](https://github.com/microsoft/windows-rs) - Windows 的 Rust 绑定
- [core-foundation-rs](https://github.com/servo/core-foundation-rs) - macOS 的 Rust 绑定
