computer_agent/desktop_mark/TAURI_REWRITE_SPEC.md
2026-06-27 17:54:44 +08:00

633 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Rust + Tauri 重构开发文档
## 1. 目标
用 Rust + Tauri 重构当前 Python + PySide6 版本,功能行为保持一致:
- 启动后显示 80×80 置顶悬浮窗。
- `Ctrl+1` 进入窗口识别模式。
- 全屏半透明遮罩覆盖虚拟屏幕。
- 鼠标移动时识别底层应用窗口,蓝色边框高亮;窗口内部无遮罩。
- 鼠标在桌面或无合格窗口时,默认选择整个虚拟屏幕。
- 鼠标点击锁定当前窗口;锁定后鼠标移动不再切换目标。
- `Esc` 或回车退出当前选择/标注流程,不删除已保存标注。
- 锁定窗口后 `Ctrl+R` 进入矩形标注模式。
- 可创建多个标注框;标注框可选中、移动、缩放。
- 左侧显示 300px 标注信息面板。
- 标注框附近显示编辑表单,可编辑控件名、描述、类型、动作、特征图占位。
- 自动保存到用户目录 JSON 文件。
首版仍只支持 Windows 10/11。
## 2. 当前 Python 版本功能清单
### 2.1 应用启动
当前入口为 `python -m desktop_mark`
启动时创建:
- `WindowProvider`Win32 窗口识别。
- `FloatingButton`80×80 悬浮窗。
- `OverlayWindow`:窗口选择与标注 overlay。
- `AnnotationPanel`:左侧标注信息面板。
- `AnnotationFormPopup`:标注编辑表单。
- `HotkeyWindow`:全局快捷键桥接。
启动后只显示悬浮窗overlay、信息面板、表单默认隐藏。
### 2.2 DPI 策略
当前 Python 版启动时调用:
- `SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)`
- Qt 高 DPI rounding policy`PassThrough`
Tauri 版应在 Rust 入口尽早设置 Win32 DPI awareness避免 Win32 坐标和 WebView 坐标偏移。
### 2.3 悬浮窗
当前行为:
- 固定尺寸80×80。
- 置顶、无边框、工具窗口。
- 深色半透明圆角背景。
- 主文本:`RPA`
- 副文本:`Ctrl+1`
- 点击等价于触发窗口选择。
- 支持鼠标拖动移动。
- 自身窗口句柄必须加入窗口识别排除列表,避免选中工具自身。
### 2.4 全局快捷键
当前快捷键:
- `Ctrl+1`:进入窗口选择模式。
- `Ctrl+R`:进入标注模式;如果没有锁定窗口,则先进入窗口选择模式。
当前实现使用 Win32 `RegisterHotKey`,不是键盘 hook。Tauri 版也应使用 Rust 原生层注册系统热键,避免管理员权限和杀软误报。
### 2.5 窗口识别
当前算法:
1. 不直接使用 `WindowFromPoint`,因为全屏 overlay 会遮住底层窗口。
2.`GetTopWindow(None)` 开始,按 Z 序循环 `GetWindow(hwnd, GW_HWNDNEXT)`
3. 跳过:
- 已排除窗口句柄。
- 不可见窗口。
- 最小化窗口。
- `WS_EX_TOOLWINDOW` 工具窗口。
- 宽高小于等于 0 的窗口。
4.`GetWindowRect(hwnd)` 得到窗口矩形。
5. 返回第一个包含鼠标点的合格顶层窗口。
6. 标题来自 `GetWindowText(hwnd)`
7. 进程名来自 `GetWindowThreadProcessId` + `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` + 模块路径文件名。
8. 没有命中任何窗口时,返回虚拟屏幕桌面窗口:
- `hwnd = null`
- `title = "Desktop"`
- `process_name = "explorer.exe"`
- `rect = virtual_screen_rect`
- `is_desktop = true`
虚拟屏幕使用:
- `SM_XVIRTUALSCREEN`
- `SM_YVIRTUALSCREEN`
- `SM_CXVIRTUALSCREEN`
- `SM_CYVIRTUALSCREEN`
### 2.6 Overlay 状态机
状态枚举:
```rust
enum OverlayMode {
Idle,
PickingWindow,
WindowLocked,
DrawingAnnotation,
EditingAnnotation,
MovingAnnotation,
ResizingAnnotation,
}
```
行为:
- `Idle`overlay 隐藏。
- `PickingWindow`overlay 显示,每 50ms 读取鼠标位置并识别窗口。
- `WindowLocked`:点击窗口后进入;蓝框保持锁定,不再跟随鼠标。
- `DrawingAnnotation`:锁定窗口内拖拽新建矩形。
- `EditingAnnotation`:显示表单,选中标注。
- `MovingAnnotation`:拖动已有标注框。
- `ResizingAnnotation`:拖动 8 个 resize handles。
### 2.7 Overlay 绘制规则
当前视觉规则:
- 全屏半透明黑色遮罩,透明度约 120/255。
- 当前目标窗口区域清除遮罩。
- 目标窗口边缘绘制 2px 蓝色边框。
- 右上角显示窗口标题和 bbox。
- 已有标注框为橙色 2px 边框。
- 标注序号显示在框左上角。
- 选中标注显示 8 个白底蓝边 resize handles。
- 新建中的临时框为蓝色 2px 边框。
Tauri 版建议:
- 用独立透明 always-on-top overlay window。
- 前端用 SVG 或 Canvas 绘制遮罩、挖空、边框、标注框。
- 遮罩挖空可用 SVG `mask` / `clipPath`,或 Canvas `globalCompositeOperation = "destination-out"`
### 2.8 标注交互
创建标注:
1. `Ctrl+R` 进入 `DrawingAnnotation`
2. 鼠标左键按下记录起点。
3. 拖动时显示临时矩形。
4. 鼠标释放:
- 对矩形做 `normalized()`
- 裁剪到锁定窗口 `clamped_to(locked_window.rect)`
- 小于 `6×6` 像素视为误触,不创建。
- 合格则添加到 `project.annotations`
- 进入 `EditingAnnotation`
- 在标注框右下附近显示编辑表单。
命中测试顺序:
1. resize handle。
2. 标注框内部移动。
3. 空白区域新建。
多个标注重叠时,最后创建的标注优先命中。
移动规则:
- 移动时保持标注框宽高不变。
- 移动结果必须完整留在锁定窗口内。
- 不允许因简单裁剪导致宽高变小。
缩放规则:
- 8 个 handles`nw``n``ne``e``se``s``sw``w`
- 最小尺寸:`6×6`
- 缩放结果裁剪到锁定窗口内。
取消规则:
- `Esc` 或回车:退出当前 overlay 流程。
- 清空当前 hover/locked/temp 状态。
- 隐藏 overlay、左侧面板、表单。
- 不删除内存中的项目数据和已保存数据。
## 3. 数据模型契约
Tauri 版 Rust 与前端必须使用同一份 JSON schema。建议 Rust `serde` 类型如下:
```rust
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WindowInfo {
pub hwnd: Option<isize>,
pub title: String,
pub process_name: String,
pub rect: Rect,
pub is_desktop: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AnnotationType {
Button,
Input,
Text,
Image,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Annotation {
pub id: String,
pub rect: Rect,
pub control_name: String,
pub description: String,
#[serde(rename = "type")]
pub annotation_type: AnnotationType,
pub actions: Vec<String>,
pub feature_preview_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AnnotationProject {
pub window: WindowInfo,
pub custom_window_name: String,
pub window_description: String,
pub annotations: Vec<Annotation>,
}
```
### 3.1 `Rect` 方法
必须实现并测试:
- `right() -> i32``x + width`
- `bottom() -> i32``y + height`
- `contains(x, y) -> bool`:使用 normalized 后半开区间 `[x, right)``[y, bottom)`
- `normalized() -> Rect`:支持负向拖拽。
- `clamped_to(bounds) -> Rect`:矩形裁剪到边界内。
- `move_clamped(dx, dy, bounds) -> Rect`:保持原宽高移动,并完整限制在边界内。
### 3.2 坐标约定
所有坐标都使用屏幕绝对物理像素:
- 窗口 bbox屏幕绝对物理像素。
- 标注框:屏幕绝对物理像素。
- JSON 保存:屏幕绝对物理像素。
首版不实现窗口移动后的标注自动重定位。
## 4. JSON 保存格式
保存路径必须兼容当前版本:
```text
%USERPROFILE%\.desktop_mark\last_project.json
```
JSON 字段固定:
```json
{
"window": {
"hwnd": 123456,
"title": "Untitled - Notepad",
"process_name": "notepad.exe",
"rect": { "x": 100, "y": 100, "width": 800, "height": 600 },
"is_desktop": false
},
"custom_window_name": "登录页",
"window_description": "用户登录窗口",
"annotations": [
{
"id": "uuid-string",
"rect": { "x": 150, "y": 160, "width": 120, "height": 32 },
"control_name": "用户名输入框",
"description": "输入登录用户名",
"type": "input",
"actions": ["input_text"],
"feature_preview_path": null
}
]
}
```
保存时机:
- 标注表单保存后。
- 窗口名称或窗口简介变化后。
加载规则:
- 启动时如果文件存在则加载到内存。
- 不自动显示 overlay。
- 加载失败时弹出错误,继续空项目。
- 不覆盖坏文件。
## 5. Tauri 推荐架构
### 5.1 窗口划分
建议使用 4 个 Tauri windows
1. `floating`
- 80×80。
- always-on-top。
- decorations false。
- transparent true。
- resizable false。
- skip taskbar true。
- 显示 `RPA` / `Ctrl+1`
2. `overlay`
- 覆盖虚拟屏幕。
- always-on-top。
- decorations false。
- transparent true。
- skip taskbar true。
- 初始隐藏。
- 用于窗口选择、遮罩、标注绘制。
3. `panel`
- 宽 300。
- 高度为虚拟屏幕高度。
- 位于虚拟屏幕左侧。
- always-on-top。
- 初始隐藏。
- 显示窗口信息和标注列表。
4. `annotation_form`
- 约 320×420。
- always-on-top。
- 初始隐藏。
- 位于标注框右下附近,越界则左/上偏移。
也可以把 `panel``annotation_form` 做成 overlay 内部 DOM但独立窗口更接近当前 PySide6 行为。
### 5.2 Rust 模块建议
```text
src-tauri/src/
main.rs
models.rs
rect.rs
win32_window.rs
hotkeys.rs
storage.rs
window_manager.rs
commands.rs
```
职责:
- `models.rs``WindowInfo``Annotation``AnnotationProject``AnnotationType`
- `rect.rs``Rect` 几何方法和单元测试。
- `win32_window.rs`:窗口枚举、进程名、虚拟屏幕。
- `hotkeys.rs``RegisterHotKey` 或 Tauri global shortcut 适配。
- `storage.rs`JSON 保存/加载。
- `window_manager.rs`:创建/定位/显示隐藏 Tauri windows。
- `commands.rs`:暴露给前端的 Tauri commands。
### 5.3 前端模块建议
```text
src/
main.tsx
state/projectStore.ts
types.ts
windows/Floating.tsx
windows/Overlay.tsx
windows/Panel.tsx
windows/AnnotationForm.tsx
geometry/rect.ts
```
职责:
- `types.ts`:与 Rust serde 对齐的类型。
- `projectStore.ts`当前项目、锁定窗口、hover 窗口、选中标注、overlay mode。
- `Overlay.tsx`:遮罩、窗口高亮、标注框、鼠标交互。
- `Panel.tsx`:窗口信息、窗口名称/简介、标注列表。
- `AnnotationForm.tsx`:控件名、描述、类型、动作、预览占位。
- `rect.ts`:前端几何算法,必须与 Rust 测试一致。
## 6. Rust Native API / Tauri Commands
建议命令:
```rust
#[tauri::command]
fn get_virtual_screen_rect() -> Rect;
#[tauri::command]
fn window_at(x: i32, y: i32, excluded_hwnds: Vec<isize>) -> WindowInfo;
#[tauri::command]
fn foreground_window(excluded_hwnds: Vec<isize>) -> Option<WindowInfo>;
#[tauri::command]
fn save_project(project: AnnotationProject) -> Result<(), String>;
#[tauri::command]
fn load_project() -> Result<Option<AnnotationProject>, String>;
#[tauri::command]
fn set_tool_window_exclusions(labels: Vec<String>) -> Result<Vec<isize>, String>;
```
前端事件:
```text
hotkey:start-window-picking
hotkey:start-annotation
project:changed
annotation:edit-requested
annotation:selected
flow:cancelled
```
## 7. Win32 实现要点
Rust 可使用 `windows` crate。
必须调用或等价实现:
- `SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)`
- `GetTopWindow(HWND(0))`
- `GetWindow(hwnd, GW_HWNDNEXT)`
- `IsWindowVisible(hwnd)`
- `IsIconic(hwnd)`
- `GetWindowLongPtrW(hwnd, GWL_EXSTYLE)`,跳过 `WS_EX_TOOLWINDOW`
- `GetWindowRect(hwnd)`
- `GetWindowTextW(hwnd)`
- `GetWindowThreadProcessId(hwnd, &mut pid)`
- `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid)`
- `QueryFullProcessImageNameW` 或等价 API 获取进程路径
- `GetSystemMetrics(SM_XVIRTUALSCREEN)` 等虚拟屏幕指标
不要把 overlay window 作为候选窗口。所有 Tauri 工具窗口 hwnd 必须加入排除集合。
## 8. UI 字段与枚举
### 8.1 标注类型
固定值:
```text
button
input
text
image
other
```
### 8.2 动作枚举
固定值:
```text
click
double_click
right_click
input_text
hover
scroll
```
### 8.3 左侧面板字段
- 窗口标题。
- 进程名。
- bbox`x, y, width, height`
- 窗口名称:可编辑。
- 窗口简介:可编辑。
- 标注列表:`序号. label (type)`
`label` 优先级:
1. `control_name`
2. `description`
3. `标注 {index}`
### 8.4 标注表单字段
- 控件名:文本输入。
- 描述:多行文本。
- 类型:下拉选择。
- 动作:多选列表。
- 特征图:占位,显示 `预览待生成`
- 保存按钮。
- 取消按钮。
保存行为:
- 更新当前 annotation。
- `feature_preview_path` 保持 `null`
- 刷新左侧列表。
- 自动保存 JSON。
- 隐藏表单。
取消行为:
- 只关闭表单。
- 不删除 annotation。
## 9. 关键行为验收
### 9.1 启动
命令启动后:
- 桌面出现 80×80 `RPA` 悬浮窗。
- overlay、panel、form 不显示。
- 无控制台异常。
### 9.2 窗口选择
准备:打开记事本和浏览器。
操作:按 `Ctrl+1`
预期:
- 全屏半透明遮罩出现。
- 鼠标移动到记事本/浏览器时,对应窗口显示蓝色边框。
- 被高亮窗口内部无遮罩。
- 鼠标移动到桌面空白时,蓝框覆盖虚拟屏幕。
### 9.3 锁定与取消
操作:在浏览器窗口上点击。
预期:
- 蓝框锁定浏览器。
- 鼠标移动到其他窗口不切换蓝框。
-`Esc` 或回车后 overlay 隐藏。
- 悬浮窗仍存在。
### 9.4 标注
操作:重新 `Ctrl+1` 锁定记事本,按 `Ctrl+R`,拖拽两个矩形。
预期:
- 两个橙色标注框出现。
- 标注框不能拖出记事本窗口。
- 选中框显示 8 个调整手柄。
- 左侧出现 300px 信息面板并列出两个标注。
### 9.5 表单与保存
操作:完成标注后,在表单输入:
- 控件名:`用户名输入框`
- 描述:`输入登录用户名`
- 类型:`input`
- 动作:`input_text`
点击保存。
预期:
- 左侧列表同步显示该标注。
- `%USERPROFILE%\.desktop_mark\last_project.json` 存在。
- JSON 包含对应 `control_name``description``type``actions`
### 9.6 边界
操作:在锁定窗口边缘向窗口外拖拽创建标注。
预期:
- 最终矩形被裁剪到锁定窗口内。
- 小于 `6×6` 的点击/拖拽不会新增标注。
## 10. 与当前 Python 版本保持一致的边界决定
- 只支持 Windows 10/11。
- 坐标保存为屏幕绝对物理像素。
- 不实现窗口移动后的标注自动重定位。
- 不实现特征图真实截图,只显示占位。
- 快捷键冲突时显示错误,不自动改用其他快捷键。
- 工具自身窗口必须被窗口识别排除。
- 没有命中应用窗口时选择整个虚拟屏幕。
## 11. 推荐开发顺序
1. 建 Tauri 项目和四个窗口。
2. 实现 Rust `Rect`、模型、JSON 保存/加载和单元测试。
3. 实现 Win32 虚拟屏幕与窗口识别。
4. 实现全局快捷键。
5. 实现 `floating` 窗口。
6. 实现 `overlay` 状态机和窗口高亮。
7. 实现标注框创建、移动、缩放。
8. 实现 `panel``annotation_form`
9. 串联自动保存和启动加载。
10. 按第 9 节逐项手工验收。
## 12. 当前 Python 版本验证基线
当前版本已验证:
```text
uv venv --python 3.11 --seed --clear
.\.venv\Scripts\python -m pip install -e .[dev]
.\.venv\Scripts\python -m pytest
```
结果:
```text
6 passed
```
启动烟测:
```text
.\.venv\Scripts\python -m desktop_mark
```
进程保持运行 3 秒,无 stdout/stderr 异常;测试后主动终止。