wechat_ai/RPA.md

417 lines
8.6 KiB
Markdown
Raw Permalink 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.

# Desktop MarkCtrl+1 后的标注逻辑技术说明
本文只总结当前项目中 **按下 `Ctrl+1` 之后** 的窗口选择与标注逻辑,不包含主页面其它功能。
下面的所有代码E:\workspace\wechat_rap
---
## 1. 入口:全局快捷键 `Ctrl+1`
代码位置:`src-tauri/src/window_capture.rs``src-tauri/src/lib.rs`
### 1.1 启动时注册快捷键
应用启动时:
-`lib.rs` 中调用 `window_capture::spawn_shortcut_listener(app.handle().clone())`
- 该函数在后台线程中通过 Win32 `RegisterHotKey` 注册:
```rust
RegisterHotKey(null_mut(), 1, MOD_CONTROL as u32, b'1' as u32)
```
含义:
- `MOD_CONTROL`Ctrl
- `b'1'`:数字键 1
- `id = 1`:该快捷键内部 ID
yu
### 1.2 消息循环监听
快捷键不是前端监听,而是 Rust 后台线程通过 Win32 消息循环监听:
```rust
while GetMessageW(&mut message, null_mut(), 0, 0) > 0 {
if message.message == WM_HOTKEY && message.wParam == 1 {
...
}
}
```
当收到 `WM_HOTKEY``wParam == 1` 时,说明用户按下了 `Ctrl+1`
---
## 2. 快捷键触发后:进入窗口选择模式
代码位置:`src-tauri/src/window_capture.rs`
按下 `Ctrl+1`Rust 会调用:
```rust
enter_window_select_mode_internal(&app, &state)
```
### 2.1 切换内部状态
该函数会更新 `OverlayStore`
```rust
store.target_window_id = None;
store.mode = OverlayMode::WindowSelect;
```
当前状态含义:
- `target_window_id = None`:还没有确认选中哪个真实应用窗口
- `mode = WindowSelect`:前端 overlay 进入“窗口选择”模式,而不是“标注框绘制”模式
### 2.2 打开 Overlay 窗口
然后调用:
```rust
show_overlay_window(app)
```
作用:
- 根据虚拟屏幕范围设置 overlay 位置与尺寸
- 显示 overlay 窗口
- 重新聚焦 overlay
Overlay 特征(见 `lib.rs`
- `transparent(true)`:透明
- `decorations(false)`:无边框
- `always_on_top(true)`:置顶
- `visible(false)`:默认隐藏,按需显示
- `skip_taskbar(true)`:不进任务栏
因此,`Ctrl+1` 的直接结果是:
```text
全屏透明置顶 Overlay 被显示出来,进入窗口选择模式
```
---
## 3. Overlay 前端:窗口选择模式如何工作
代码位置:`src/overlay.ts``overlay.html``src/overlay.css`
### 3.1 Overlay 的数据来源
前端通过轮询:
```ts
invoke<OverlayFrame>('get_overlay_frame')
```
从 Rust 获取当前帧状态。
`OverlayFrame` 关键字段:
- `mode`: `'annotation' | 'window_select'`
- `virtual_screen_rect`
- `target_window`
- `hover_window`
- `cursor`
- `annotations`
- `node_tree`
`Ctrl+1` 后:
```text
mode = window_select
```
### 3.2 鼠标移到哪个窗口,哪个窗口高亮
Rust 侧通过:
```rust
hover_target()
```
获取鼠标所在真实窗口。
逻辑要点:
1. 先用 `GetCursorPos` 取屏幕坐标
2. 再通过 `WindowFromPoint` + `GetAncestor(..., GA_ROOT)` 拿到鼠标下的顶层窗口
3. 再向下过滤,跳过:
- Desktop Mark 自己
- 工具窗口
- 输入法 / 任务栏 / 桌面 / 阴影等系统窗口
4. 最终得到一个可选的真实桌面应用窗口 `hover_window`
### 3.3 前端高亮绘制
`overlay.ts` 里:
- 整屏先绘制半透明遮罩
- 当前 `hover_window` 对应区域被镂空
- 再绘制高亮边框与提示标签
窗口选择模式下,标签文本类似:
```text
点击选择:窗口标题 · x=... , y=... · DPI ...%
```
因此用户看到的体验是:
```text
Ctrl+1 → 整屏暗化 → 鼠标移到哪个应用窗口,哪个窗口被高亮
```
---
## 4. 点击窗口:确认目标窗口
代码位置:`src/overlay.ts``src-tauri/src/window_capture.rs`
### 4.1 前端点击逻辑
`overlay.ts` 中,`pointerup` 时如果当前模式是 `window_select`
```ts
await invoke('select_target_window', { windowId: frame.hover_window.window_id });
frame = await invoke<OverlayFrame>('get_overlay_frame');
```
也就是说:
- 用户点击当前高亮窗口
- 把这个窗口的 `window_id` 发送给 Rust
### 4.2 Rust 侧确认目标窗口
对应命令:
```rust
select_target_window(window_id, app, state)
```
它会做三件事:
#### a. 校验窗口存在
```rust
find_window_by_id(&window_id)
```
#### b. 把目标窗口提到前台
```rust
bring_window_to_front(window.hwnd)
```
内部会:
- `ShowWindow(hwnd, SW_RESTORE)` 恢复窗口
- `SetWindowPos(... HWND_TOPMOST ...)` 暂时提到最上层
- `SetForegroundWindow(hwnd)` 设为前台
#### c. 切换 Overlay 模式
```rust
store.target_window_id = Some(window_id);
store.mode = OverlayMode::Annotation;
```
此时进入:
```text
标注模式Annotation
```
#### d. 再次显示并聚焦 Overlay
```rust
show_overlay_window(&app)
```
这样可以避免用户选完窗口后还要额外点一下别处,才能开始画标注框。
---
## 5. 标注模式:只显示当前目标窗口的标注框
代码位置:`src-tauri/src/window_capture.rs`
用户确认窗口后,后续 `get_overlay_frame()` 不会再返回所有标注,而是只返回:
```rust
当前 target_window_id 对应的 annotations
```
实现逻辑:
```rust
let visible_annotations = if store.mode == OverlayMode::Annotation {
store
.target_window_id
.as_ref()
.map(|window_id| {
store
.annotations
.iter()
.filter(|annotation| &annotation.window_id == window_id)
.cloned()
.collect()
})
.unwrap_or_default()
} else {
Vec::new()
};
```
效果:
- 切到窗口 A 标注时,只显示窗口 A 的标注框
- 切到窗口 B 标注时,窗口 A 的标注框会隐藏,但数据不会丢
---
## 6. 在目标窗口内拖拽创建标注框
代码位置:`src/overlay.ts``src-tauri/src/window_capture.rs`
### 6.1 前端拖拽
`annotation` 模式下:
- `pointerdown` 记录起点
- `pointermove` 更新终点
- `pointerup` 生成屏幕坐标矩形 `RectInfo`
然后调用:
```ts
invoke('add_annotation', {
input: {
window_id: frame.target_window.window_id,
screen_rect: rect,
label: null,
action_kind: 'click',
description: null,
}
})
```
### 6.2 Rust 保存标注
命令:
```rust
add_annotation(input, state)
```
Rust 会做这些校验:
1. 目标窗口是否仍存在
2. 标注框尺寸是否太小
3. 标注框是否完整位于目标窗口内部
通过后,生成:
- `annotation.id`
- `label`
- `action_kind`
- `description`
- `screen_rect`
- `normalized_rect`
- `feature_image_path`
### 6.3 特征图生成
保存标注时还会生成一个本地 BMP 特征图:
```rust
capture_feature_image(&format!("ann-{index}"), input.screen_rect)
```
特征图保存目录:
```text
%TEMP%/desktop_mark/feature_images/
```
---
## 7. 标注数据如何持久化
代码位置:`src-tauri/src/window_capture.rs`
当前标注逻辑不是只存在内存里,而是会持久化到本地:
### 7.1 状态文件
```text
./data/workflow-state.json
```
### 7.2 持久化内容
`PersistedWorkflowState` 包含:
- `app_name`
- `app_description`
- `annotations`
- `next_annotation_index`
### 7.3 触发时机
以下操作后都会持久化:
- `add_annotation`
- `update_annotation`
- `update_annotation_geometry`
- `delete_annotation`
- `clear_annotations`
- `update_app_metadata`
---
## 8. Ctrl+1 后逻辑的完整时序
```text
用户按下 Ctrl+1
→ Rust Win32 热键线程收到 WM_HOTKEY
→ 进入 WindowSelect 模式
→ 显示全屏透明 Overlay
→ 鼠标移动Rust 持续计算 hover_window
→ 前端绘制高亮窗口
→ 用户点击高亮窗口
→ Rust 记录 target_window_id并切到 Annotation 模式
→ Overlay 再次聚焦
→ 用户直接在目标窗口内拖拽
→ 前端把矩形发给 add_annotation
→ Rust 校验并保存 annotation + feature image
→ 当前窗口的标注框持续显示,其它窗口标注框隐藏
```
---
## 9. 当前 Ctrl+1 方案的核心特点
### 优点
- 全局快捷键直接进入窗口选择,不依赖前端焦点
- Overlay 与真实应用窗口分离,不污染目标应用
- 只显示当前目标窗口的标注框,切窗不混乱
- 标注数据与特征图会落本地
- DPI、虚拟屏、多窗口过滤都在 Rust/Win32 层处理
### 当前限制
- 目标窗口关系、节点关系仍主要依赖本地状态和后续页面逻辑
- Ctrl+1 只负责“选窗 + 进入标注模式”,不负责主页面流程编排
- feature image 当前保存为 BMP不是 PNG
---
## 10. 涉及文件清单
### Rust 后端
- `src-tauri/src/lib.rs`
- `src-tauri/src/window_capture.rs`
### Overlay 前端
- `overlay.html`
- `src/overlay.ts`
- `src/overlay.css`
### 类型定义
- `src/types.ts`