feat(agent): 相对鼠标注入(读光标+增量+clamp 绝对注入)+ 显式 DPI 感知
注入实现刻意**不用** SendInput(MOUSEEVENTF_MOVE):实测该路径的 dx 不是 像素而是 mickey,会被「指针加速」与「DPI 缩放」双重放大——本机 dx=1 走 0px、 dx=20 走 41px、dx=100 走 309px,且倍率随用户系统设置与机器变化,无法标定。 改为「GetCursorPos + 增量 + clamp + 绝对注入」,实测 1:1 精确(±1/2/3/5/17/100 四次重复全部等于请求值),撞边后回滑立即响应、不积步数债。 同时显式设 PER_MONITOR_AWARE_V2:实测本机 agent.exe 虽是 per-monitor, 但那是注册表 AppCompatFlags\\Layers 里一条**路径绑定**的 HIGHDPIAWARE 覆盖 造成的(同一 exe 复制到 %TEMP% 启动即变 UNAWARE),换机器/换构建路径即失效。 UNAWARE 时 GetCursorPos 返回逻辑坐标(1536x864)而采集是物理像素(1920x1080), clamp 上界与增量尺度都会错。设置失败时区分「已被外部设定」与「真正降级」并分别报日志。
This commit is contained in:
parent
39af42e1b3
commit
78ac9d9972
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1434,6 +1434,7 @@ dependencies = [
|
|||||||
"quinn",
|
"quinn",
|
||||||
"rustls",
|
"rustls",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -24,4 +24,7 @@ windows = { workspace = true, features = [
|
|||||||
"Win32_Graphics_Dxgi_Common",
|
"Win32_Graphics_Dxgi_Common",
|
||||||
"Win32_UI_Input_KeyboardAndMouse",
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
|
# SetProcessDpiAwarenessContext:相对鼠标的 GetCursorPos 必须与采集同为物理像素,
|
||||||
|
# 不能依赖注册表 AppCompat 覆盖(见 3b 开工实测 §4)。
|
||||||
|
"Win32_UI_HiDpi",
|
||||||
] }
|
] }
|
||||||
|
|||||||
@ -9,9 +9,10 @@ use windows::Win32::UI::Input::KeyboardAndMouse::{
|
|||||||
MOUSE_EVENT_FLAGS,
|
MOUSE_EVENT_FLAGS,
|
||||||
};
|
};
|
||||||
use windows::Win32::UI::WindowsAndMessaging::{
|
use windows::Win32::UI::WindowsAndMessaging::{
|
||||||
GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN,
|
GetCursorPos, GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_CXSCREEN,
|
||||||
SM_YVIRTUALSCREEN,
|
SM_CYSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
|
||||||
};
|
};
|
||||||
|
use windows::Win32::Foundation::POINT;
|
||||||
|
|
||||||
// windows 0.58 未导出 LEFT 常量,按 WinUser.h 原值定义
|
// windows 0.58 未导出 LEFT 常量,按 WinUser.h 原值定义
|
||||||
const MOUSEEVENTF_LEFTDOWN: u32 = 0x0002;
|
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) {
|
fn send_key(scancode: u16, extended: bool, up: bool) {
|
||||||
let mut flags = KEYEVENTF_SCANCODE;
|
let mut flags = KEYEVENTF_SCANCODE;
|
||||||
if extended {
|
if extended {
|
||||||
@ -167,6 +224,7 @@ pub fn handle(ev: InputEvent) {
|
|||||||
}
|
}
|
||||||
InputEvent::KeyDown { scancode, extended } => send_key(scancode, extended, false),
|
InputEvent::KeyDown { scancode, extended } => send_key(scancode, extended, false),
|
||||||
InputEvent::KeyUp { scancode, extended } => send_key(scancode, extended, true),
|
InputEvent::KeyUp { scancode, extended } => send_key(scancode, extended, true),
|
||||||
|
InputEvent::MouseMoveRel { dx, dy } => send_mouse_rel(dx, dy),
|
||||||
InputEvent::Text { units } => {
|
InputEvent::Text { units } => {
|
||||||
// 上限截断必须可见(与 agent-core::dispatch_input 同口径;Windows 端
|
// 上限截断必须可见(与 agent-core::dispatch_input 同口径;Windows 端
|
||||||
// 走自己的 handle(),不经过 agent-core,所以这里同源补齐——R-3 教训)。
|
// 走自己的 handle(),不经过 agent-core,所以这里同源补齐——R-3 教训)。
|
||||||
|
|||||||
@ -32,8 +32,60 @@ fn parse_args() -> Result<(u16, String)> {
|
|||||||
Ok((port, code))
|
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]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
// 第一件事:定下 DPI 感知,后续所有坐标运算都依赖它
|
||||||
|
init_dpi_awareness();
|
||||||
let (port, code) = parse_args()?;
|
let (port, code) = parse_args()?;
|
||||||
rustls::crypto::ring::default_provider()
|
rustls::crypto::ring::default_provider()
|
||||||
.install_default()
|
.install_default()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user