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:
郑博文 committed 2026-10-31 17:34:54 +08:00
1 parent 39af42e1b3
commit 78ac9d9972
4 files changed
+116 -2

No files matched your search

+52
View File
@@ -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()