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

16 KiB
Raw Permalink Blame History

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

启动时创建:

  • WindowProviderWin32 窗口识别。
  • FloatingButton80×80 悬浮窗。
  • OverlayWindow:窗口选择与标注 overlay。
  • AnnotationPanel:左侧标注信息面板。
  • AnnotationFormPopup:标注编辑表单。
  • HotkeyWindow:全局快捷键桥接。

启动后只显示悬浮窗overlay、信息面板、表单默认隐藏。

2.2 DPI 策略

当前 Python 版启动时调用:

  • SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
  • Qt 高 DPI rounding policyPassThrough

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 状态机

状态枚举:

enum OverlayMode {
    Idle,
    PickingWindow,
    WindowLocked,
    DrawingAnnotation,
    EditingAnnotation,
    MovingAnnotation,
    ResizingAnnotation,
}

行为:

  • Idleoverlay 隐藏。
  • PickingWindowoverlay 显示,每 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 个 handlesnwnneesessww
  • 最小尺寸:6×6
  • 缩放结果裁剪到锁定窗口内。

取消规则:

  • Esc 或回车:退出当前 overlay 流程。
  • 清空当前 hover/locked/temp 状态。
  • 隐藏 overlay、左侧面板、表单。
  • 不删除内存中的项目数据和已保存数据。

3. 数据模型契约

Tauri 版 Rust 与前端必须使用同一份 JSON schema。建议 Rust serde 类型如下:

#[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() -> i32x + width
  • bottom() -> i32y + 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 保存格式

保存路径必须兼容当前版本:

%USERPROFILE%\.desktop_mark\last_project.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。
    • 初始隐藏。
    • 位于标注框右下附近,越界则左/上偏移。

也可以把 panelannotation_form 做成 overlay 内部 DOM但独立窗口更接近当前 PySide6 行为。

5.2 Rust 模块建议

src-tauri/src/
  main.rs
  models.rs
  rect.rs
  win32_window.rs
  hotkeys.rs
  storage.rs
  window_manager.rs
  commands.rs

职责:

  • models.rsWindowInfoAnnotationAnnotationProjectAnnotationType
  • rect.rsRect 几何方法和单元测试。
  • win32_window.rs:窗口枚举、进程名、虚拟屏幕。
  • hotkeys.rsRegisterHotKey 或 Tauri global shortcut 适配。
  • storage.rsJSON 保存/加载。
  • window_manager.rs:创建/定位/显示隐藏 Tauri windows。
  • commands.rs:暴露给前端的 Tauri commands。

5.3 前端模块建议

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

建议命令:

#[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>;

前端事件:

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 标注类型

固定值:

button
input
text
image
other

8.2 动作枚举

固定值:

click
double_click
right_click
input_text
hover
scroll

8.3 左侧面板字段

  • 窗口标题。
  • 进程名。
  • bboxx, 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_namedescriptiontypeactions

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. 实现 panelannotation_form
  9. 串联自动保存和启动加载。
  10. 按第 9 节逐项手工验收。

12. 当前 Python 版本验证基线

当前版本已验证:

uv venv --python 3.11 --seed --clear
.\.venv\Scripts\python -m pip install -e .[dev]
.\.venv\Scripts\python -m pytest

结果:

6 passed

启动烟测:

.\.venv\Scripts\python -m desktop_mark

进程保持运行 3 秒,无 stdout/stderr 异常;测试后主动终止。