feat(common): 协议 v4 新增 MouseMoveRel 相对鼠标事件

v4 新增 InputEvent::MouseMoveRel{dx,dy},单位为被控端桌面物理像素,
语义是「把远端指针从当前位置移动 (dx,dy)」,越界由被控端 clamp。

为什么必须升版本:bincode 按位置编码枚举判别符,新增变体会改变其后
变体的编号,旧端会静默错解成别的输入事件。v4 后旧 v3 端由 Auth 握手
显式拒绝(已用 v3 二进制实测:client=3, server=4)。

为什么单位是像素而不是归一化增量:AuthOk 已下发桌面宽高,主控端换算
一次即可;被控端拿到像素可直接与 GetCursorPos 同尺度相加,少一次取整。

单测锁住:版本号、负值往返(i16 全范围)、与绝对 MouseMove 不混淆。
This commit is contained in:
郑博文 2026-10-31 17:34:47 +08:00
parent 114e8431a6
commit 39af42e1b3

View File

@ -19,7 +19,10 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
/// - v3:新增 `InputEvent::Text`(文本注入通道,UTF-16 code unit)。
/// `InputEvent` 因携带 `Vec<u16>` 不再是 `Copy`;三端必须同步升级,
/// 旧端由 `Auth` 握手显式拒绝。
pub const PROTO_VERSION: u16 = 3;
/// - v4:新增 `InputEvent::MouseMoveRel`(相对鼠标,单位=被控端桌面物理像素)。
/// bincode 按位置编码枚举判别序号,新增变体会改变其后变体的编号,
/// 故同样必须三端同步;旧端由 `Auth` 握手显式拒绝。
pub const PROTO_VERSION: u16 = 4;
/// 控制通道消息(双向)
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -78,6 +81,25 @@ pub enum InputEvent {
///
/// 日志纪律:中间层与注入端只允许打印 `units.len()`,**不得**打印用户输入内容。
Text { units: Vec<u16> },
/// v4 新增:**相对**鼠标位移,单位是**被控端桌面的物理像素**(不是归一化值)。
///
/// 语义:把远端指针从**当前位置**移动 `(dx, dy)`,越界部分由被控端 clamp 到桌面内。
/// 主控端只说「移动多少」,不说「去哪里」——远端指针位置始终是唯一权威,
/// 本机真鼠标同时移动也不会造成跳变。
///
/// 为什么单位是像素而不是归一化增量:`AuthOk` 已下发桌面 `width`/`height`,
/// 主控端换算一次即可;被控端拿到像素可直接与 `GetCursorPos` 同尺度相加,
/// 少一次取整(每多一次取整就多一个累积误差源)。
///
/// 为什么是 `i16`:单次触摸事件的位移远小于 32767;用定长整数让线格式保持紧凑。
///
/// **与 [`InputEvent::MouseMove`] 互斥**:绝对模式发 `MouseMove`,相对模式发本变体,
/// 由主控端的模式开关决定,同一会话内不应混用(混用不会出错,但手感会跳)。
///
/// 注入实现注意:Windows 上**不能**直接 `SendInput(MOUSEEVENTF_MOVE)` 透传
/// ——实测该路径被「指针加速」与「DPI 缩放」双重放大(本机 dx=100 实际走了 309px,
/// 且倍率随系统设置变化)。正确做法见 `agent/src/input.rs::send_mouse_rel`。
MouseMoveRel { dx: i16, dy: i16 },
}
/// [`InputEvent::Text`] 单次携带的 code unit 上限。
@ -313,10 +335,58 @@ mod tests {
// ---- v3 文本注入通道 ----
/// 版本守卫:v3 是本轮契约变更的登记点,改动必须显式经过这里。
/// 版本守卫:v3 是文本通道的登记点。v4 之后此断言随版本推进,
/// 保留它的作用是「任何一次版本变更都必须显式经过这里」。
#[test]
fn proto_version_is_three_after_text_event() {
assert_eq!(PROTO_VERSION, 3, "新增 InputEvent::Text 后必须递增到 3");
assert!(
PROTO_VERSION >= 3,
"Text 通道自 v3 起存在,版本不得回退(当前 {PROTO_VERSION})"
);
}
// ---- v4 相对鼠标 ----
/// 版本守卫:v4 是相对鼠标的登记点。
#[test]
fn proto_version_is_four_after_rel_move() {
assert_eq!(
PROTO_VERSION, 4,
"新增 InputEvent::MouseMoveRel 后必须递增到 4"
);
}
/// 相对位移必须原样往返,**包含负值**(向左/向上移动)。
/// 负值是本变体的核心语义:绝对坐标是无符号的,相对增量必须有符号。
#[test]
fn mouse_move_rel_roundtrips_signed_deltas() {
for (dx, dy) in [(0i16, 0i16), (1, -1), (-32768, 32767), (100, -250)] {
let m = ControlMsg::Input(InputEvent::MouseMoveRel { dx, dy });
let bytes = encode_control(&m);
let back: ControlMsg = bincode::deserialize(&bytes[4..]).unwrap();
match back {
ControlMsg::Input(InputEvent::MouseMoveRel { dx: gx, dy: gy }) => {
assert_eq!((gx, gy), (dx, dy), "相对增量往返失真");
}
other => panic!("期望 Input::MouseMoveRel,实际 {other:?}"),
}
}
}
/// 判别序号守卫:`MouseMoveRel` 必须与既有变体**互不混淆**。
/// bincode 按位置编码枚举判别符,若新变体插在中间会让旧端静默错解——
/// 这条测试锁住「新增变体后各变体仍各自往返」。
#[test]
fn rel_move_does_not_alias_other_input_events() {
let rel = ControlMsg::Input(InputEvent::MouseMoveRel { dx: 7, dy: -9 });
let abs = ControlMsg::Input(InputEvent::MouseMove { x: 7, y: 9 });
let rb = encode_control(&rel);
let ab = encode_control(&abs);
assert_ne!(rb, ab, "相对与绝对移动的线格式不得相同");
match bincode::deserialize::<ControlMsg>(&rb[4..]).unwrap() {
ControlMsg::Input(InputEvent::MouseMoveRel { .. }) => {}
other => panic!("相对移动被错解为 {other:?}"),
}
}
#[test]