diff --git a/Cargo.lock b/Cargo.lock index 4197002..ff2a474 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1434,6 +1434,7 @@ dependencies = [ "quinn", "rustls", "tokio", + "windows", ] [[package]] diff --git a/agent/Cargo.toml b/agent/Cargo.toml index f8b4e0a..61c1a16 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -24,4 +24,7 @@ windows = { workspace = true, features = [ "Win32_Graphics_Dxgi_Common", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging", + # SetProcessDpiAwarenessContext:相对鼠标的 GetCursorPos 必须与采集同为物理像素, + # 不能依赖注册表 AppCompat 覆盖(见 3b 开工实测 §4)。 + "Win32_UI_HiDpi", ] } diff --git a/agent/src/input.rs b/agent/src/input.rs index 3612f44..0539853 100644 --- a/agent/src/input.rs +++ b/agent/src/input.rs @@ -9,9 +9,10 @@ use windows::Win32::UI::Input::KeyboardAndMouse::{ MOUSE_EVENT_FLAGS, }; use windows::Win32::UI::WindowsAndMessaging::{ - GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, - SM_YVIRTUALSCREEN, + GetCursorPos, GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_CXSCREEN, + SM_CYSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, }; +use windows::Win32::Foundation::POINT; // windows 0.58 未导出 LEFT 常量,按 WinUser.h 原值定义 const MOUSEEVENTF_LEFTDOWN: u32 = 0x0002; @@ -36,6 +37,62 @@ fn send_mouse(flags: u32, dx: i32, dy: i32, data: u32) { } } +/// 主显示器物理尺寸(像素)。相对移动的 clamp 上界必须与**采集空间**一致 +/// (capture 用 `EnumOutputs(0)`;AuthOk 下发的 width/height 也来自它)。 +fn output_size() -> (i32, i32) { + unsafe { + ( + GetSystemMetrics(SM_CXSCREEN), + GetSystemMetrics(SM_CYSCREEN), + ) + } +} + +/// v4 相对鼠标:把远端指针从当前位置移动 `(dx, dy)` 像素。 +/// +/// **为什么不用 `SendInput(MOUSEEVENTF_MOVE)`**(协议 v4 的语义最容易在这里跑偏): +/// 实测(2026-10-31,见个人层 `docs/output/2026-10-31-3b-开工前实测.md`)该路径的 +/// `dx` 不是像素,而是「mickey」,会被两件事放大: +/// 1. **指针加速**(「提高指针精确度」)。本机 `SPI_GETMOUSE=6/10/1` 下 +/// dx=20 实际走 41px、dx=100 走 309px,且倍率是非线性的; +/// 2. **DPI 缩放**。关掉加速后倍率恒为 1.25 = 本机 125% 缩放。 +/// 两者都随用户系统设置与机器变化,无法标定,因此**不能**把协议里的像素增量直接透传。 +/// +/// 改为「读光标 → 加增量 → clamp → 绝对注入」: +/// - 语义仍是**相对**(主控端只说移动多少,不说去哪里),远端指针始终权威; +/// - 实测 1:1 精确(dx=±1/2/3/5/17/100 四次重复全部等于请求值); +/// - 撞边后回滑立即响应,不积累「步数债」(clamp 丢掉的部分不回补)。 +/// +/// 已知限制(不隐藏):读光标与注入之间若用户同时移动真鼠标,会有一次增量被叠加。 +/// 本机联调场景可接受;彻底消除需要 `SendInput` 的 mickey 路径,而那条路不可标定。 +fn send_mouse_rel(dx: i16, dy: i16) { + if dx == 0 && dy == 0 { + return; // 零位移没有注入语义,也不该产生一次系统调用 + } + let (w, h) = output_size(); + if w <= 1 || h <= 1 { + eprintln!("[input] 相对移动失败:输出尺寸异常 {w}x{h}"); + return; + } + let mut cur = POINT::default(); + // GetCursorPos 失败时不能猜一个坐标继续注入——那会把指针扔到屏幕角落 + if let Err(e) = unsafe { GetCursorPos(&mut cur) } { + eprintln!("[input] GetCursorPos 失败,相对移动被丢弃: {e}"); + return; + } + let tx = (cur.x + dx as i32).clamp(0, w - 1); + let ty = (cur.y + dy as i32).clamp(0, h - 1); + // 归一化到 0..=65535(绝对注入空间 = 主显示器,不带 VIRTUALDESK,见 MouseMove 分支) + let nx = ((tx as i64 * 65535 + (w as i64 - 1) / 2) / (w as i64 - 1)) as i32; + let ny = ((ty as i64 * 65535 + (h as i64 - 1) / 2) / (h as i64 - 1)) as i32; + send_mouse( + MOUSEEVENTF_ABSOLUTE.0 | MOUSEEVENTF_MOVE.0, + nx, + ny, + 0, + ); +} + fn send_key(scancode: u16, extended: bool, up: bool) { let mut flags = KEYEVENTF_SCANCODE; if extended { @@ -167,6 +224,7 @@ pub fn handle(ev: InputEvent) { } InputEvent::KeyDown { scancode, extended } => send_key(scancode, extended, false), InputEvent::KeyUp { scancode, extended } => send_key(scancode, extended, true), + InputEvent::MouseMoveRel { dx, dy } => send_mouse_rel(dx, dy), InputEvent::Text { units } => { // 上限截断必须可见(与 agent-core::dispatch_input 同口径;Windows 端 // 走自己的 handle(),不经过 agent-core,所以这里同源补齐——R-3 教训)。 diff --git a/agent/src/main.rs b/agent/src/main.rs index cffe7d0..e6d0368 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -32,8 +32,60 @@ fn parse_args() -> Result<(u16, String)> { Ok((port, code)) } +/// 显式声明进程 DPI 感知(**必须在任何采集/注入调用之前**,越早越好)。 +/// +/// 为什么不能省(3b 开工实测,见个人层 `docs/output/2026-10-31-3b-开工前实测.md` §4): +/// 相对鼠标走「`GetCursorPos` + 增量」,而 `GetCursorPos` 的坐标空间取决于进程的 +/// DPI 感知模式——UNAWARE 进程拿到的是**逻辑坐标**(本机 125% 缩放下是 1536x864), +/// 而 DXGI 采集与 `AuthOk` 下发的是**物理像素**(1920x1080)。两者混用会让 +/// clamp 上界偏小、增量尺度偏小:指针到不了屏幕右/下边缘,且移动距离只有 80%。 +/// +/// 为什么不能依赖现状:本机 `agent.exe` 实测确实是 `PER_MONITOR_AWARE(2)`,但那是 +/// `HKCU\...\AppCompatFlags\Layers` 里一条**路径绑定**的 `HIGHDPIAWARE` 覆盖造成的 +/// ——同一个 exe 复制到 `%TEMP%` 启动就变成 `UNAWARE(0)`。换机器、换构建路径、 +/// CI 产物、用户机器上都不成立,所以必须由代码显式设置。 +/// +/// 失败时只告警不退出:DPI 感知不可用是老系统(< Win10 1703)的情况, +/// 此时相对鼠标会退化(精度下降),但绝对注入与其余功能不受影响—— +/// 让整个被控端起不来是更差的选择。 +fn init_dpi_awareness() { + use windows::Win32::UI::HiDpi::{ + GetThreadDpiAwarenessContext, SetProcessDpiAwarenessContext, GetAwarenessFromDpiAwarenessContext, + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, + }; + match unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) } { + Ok(()) => println!("[agent] DPI awareness: PER_MONITOR_AWARE_V2 (set by this process)"), + Err(e) => { + // 失败有两种完全不同的原因,**必须分开报告**,否则日志会误导排查方向: + // a) 进程的感知模式已被别处设定(注册表 AppCompat 覆盖、清单、父进程)—— + // ERROR_ACCESS_DENIED(0x80070005),此时若已经是 per-monitor 就完全没问题; + // b) 系统过旧不支持该 API——这才是真正的降级。 + // 本机实测是 (a):HKCU 的 AppCompatFlags\Layers 里有一条路径绑定的 HIGHDPIAWARE。 + let actual = unsafe { + GetAwarenessFromDpiAwarenessContext(GetThreadDpiAwarenessContext()) + }; + // DPI_AWARENESS(WinUser.h):INVALID=-1、UNAWARE=0、SYSTEM_AWARE=1、 + // PER_MONITOR_AWARE=2。注意 PER_MONITOR 是 **2** 而不是 3。 + let is_per_monitor = actual.0 == 2; + if is_per_monitor { + println!( + "[agent] DPI awareness: 已由外部设定为 PER_MONITOR_AWARE(无需本进程设置;{e})" + ); + } else { + eprintln!( + "[agent] 警告:DPI 感知为 {}(非 per-monitor),且本进程设置失败({e})。\ + 相对鼠标的位移比例会与被采集桌面不一致;绝对触摸不受影响。", + actual.0 + ); + } + } + } +} + #[tokio::main] async fn main() -> Result<()> { + // 第一件事:定下 DPI 感知,后续所有坐标运算都依赖它 + init_dpi_awareness(); let (port, code) = parse_args()?; rustls::crypto::ring::default_provider() .install_default()