2026-10-29 23:29:25 -04:00
|
|
|
|
//! 输入注入:SendInput 实现(鼠标归一化绝对坐标 + 扫描码键盘 + v3 文本注入)。
|
2026-10-22 21:17:55 -04:00
|
|
|
|
|
|
|
|
|
|
use common::{InputEvent, MouseButton};
|
|
|
|
|
|
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
|
|
|
|
|
SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY,
|
2026-10-29 23:29:25 -04:00
|
|
|
|
KEYEVENTF_KEYUP, KEYEVENTF_SCANCODE, KEYEVENTF_UNICODE, MOUSEEVENTF_ABSOLUTE,
|
|
|
|
|
|
MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN,
|
|
|
|
|
|
MOUSEEVENTF_RIGHTUP, MOUSEEVENTF_VIRTUALDESK, MOUSEEVENTF_WHEEL, MOUSEINPUT, VIRTUAL_KEY,
|
|
|
|
|
|
MOUSE_EVENT_FLAGS,
|
2026-10-22 21:17:55 -04:00
|
|
|
|
};
|
|
|
|
|
|
use windows::Win32::UI::WindowsAndMessaging::{
|
2026-10-31 05:34:54 -04:00
|
|
|
|
GetCursorPos, GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_CXSCREEN,
|
|
|
|
|
|
SM_CYSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
|
2026-10-22 21:17:55 -04:00
|
|
|
|
};
|
2026-10-31 05:34:54 -04:00
|
|
|
|
use windows::Win32::Foundation::POINT;
|
2026-10-22 21:17:55 -04:00
|
|
|
|
|
|
|
|
|
|
// windows 0.58 未导出 LEFT 常量,按 WinUser.h 原值定义
|
|
|
|
|
|
const MOUSEEVENTF_LEFTDOWN: u32 = 0x0002;
|
|
|
|
|
|
const MOUSEEVENTF_LEFTUP: u32 = 0x0004;
|
|
|
|
|
|
|
|
|
|
|
|
fn send_mouse(flags: u32, dx: i32, dy: i32, data: u32) {
|
|
|
|
|
|
let inp = INPUT {
|
|
|
|
|
|
r#type: INPUT_MOUSE,
|
|
|
|
|
|
Anonymous: INPUT_0 {
|
|
|
|
|
|
mi: MOUSEINPUT {
|
|
|
|
|
|
dx,
|
|
|
|
|
|
dy,
|
|
|
|
|
|
mouseData: data,
|
|
|
|
|
|
dwFlags: MOUSE_EVENT_FLAGS(flags),
|
|
|
|
|
|
time: 0,
|
|
|
|
|
|
dwExtraInfo: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
unsafe {
|
|
|
|
|
|
SendInput(&[inp], std::mem::size_of::<INPUT>() as i32);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-31 05:34:54 -04:00
|
|
|
|
/// 主显示器物理尺寸(像素)。相对移动的 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,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 21:17:55 -04:00
|
|
|
|
fn send_key(scancode: u16, extended: bool, up: bool) {
|
|
|
|
|
|
let mut flags = KEYEVENTF_SCANCODE;
|
|
|
|
|
|
if extended {
|
|
|
|
|
|
flags |= KEYEVENTF_EXTENDEDKEY;
|
|
|
|
|
|
}
|
|
|
|
|
|
if up {
|
|
|
|
|
|
flags |= KEYEVENTF_KEYUP;
|
|
|
|
|
|
}
|
|
|
|
|
|
let inp = INPUT {
|
|
|
|
|
|
r#type: INPUT_KEYBOARD,
|
|
|
|
|
|
Anonymous: INPUT_0 {
|
|
|
|
|
|
ki: KEYBDINPUT {
|
|
|
|
|
|
wVk: VIRTUAL_KEY(0),
|
|
|
|
|
|
wScan: scancode,
|
|
|
|
|
|
dwFlags: flags,
|
|
|
|
|
|
time: 0,
|
|
|
|
|
|
dwExtraInfo: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
unsafe {
|
|
|
|
|
|
SendInput(&[inp], std::mem::size_of::<INPUT>() as i32);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 打印虚拟桌面信息(调试用)
|
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
|
|
pub fn virtual_desktop() -> (i32, i32, i32, i32) {
|
|
|
|
|
|
unsafe {
|
|
|
|
|
|
(
|
|
|
|
|
|
GetSystemMetrics(SM_XVIRTUALSCREEN),
|
|
|
|
|
|
GetSystemMetrics(SM_YVIRTUALSCREEN),
|
|
|
|
|
|
GetSystemMetrics(SM_CXVIRTUALSCREEN),
|
|
|
|
|
|
GetSystemMetrics(SM_CYVIRTUALSCREEN),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-29 23:29:25 -04:00
|
|
|
|
/// v3 文本注入:把一段 UTF-16 code unit 作为键盘事件送入前台应用。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 语义来源(实读,不凭记忆):
|
|
|
|
|
|
/// [KEYBDINPUT (winuser.h)](https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-keybdinput)
|
|
|
|
|
|
/// - `KEYEVENTF_UNICODE`(0x0004):`wVk` **必须为 0**,`wScan` 即要送出的 Unicode 字符;
|
|
|
|
|
|
/// 系统合成 `VK_PACKET` 按键事件,经 `TranslateMessage` 转为带该字符的 `WM_CHAR`。
|
2026-10-29 23:41:54 -04:00
|
|
|
|
/// - 文档原文是 "This flag can only be combined with the KEYEVENTF_KEYUP flag"。
|
|
|
|
|
|
/// 本项目**仍先发不带 KEYUP 的 down**(与现有 `send_key` 的 down/up 成对形态一致),
|
|
|
|
|
|
/// 把那句读作「不能与 SCANCODE/EXTENDEDKEY 混用」是**我的推断,文档没有明说**。
|
|
|
|
|
|
/// 若真机验收发现 down 事件被系统丢弃(表现为一个字符进两次或不进),
|
|
|
|
|
|
/// 退路是只发 `KEYEVENTF_UNICODE | KEYEVENTF_KEYUP` 单事件——改一处 `mk` 即可。
|
2026-10-29 23:29:25 -04:00
|
|
|
|
///
|
|
|
|
|
|
/// 为什么一次性批量提交(而非逐字符一对 down/up):
|
|
|
|
|
|
/// 代理对(U+1F600 = D83D DE00)的两个 code unit 必须在输入队列里**相邻**,
|
|
|
|
|
|
/// 远端才会把它们合成成一个字符。逐字符成对(down,up,down,up)会在两个
|
2026-10-29 23:41:54 -04:00
|
|
|
|
/// down 之间插入 up 事件;Windows 对 VK_PACKET 孤立代理项的缓冲规则文档
|
|
|
|
|
|
/// 没有描述,所以这里选择让两个 down 严格相邻的形态,由真机 emoji 验收(V7)定论。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 为什么是**一次** `SendInput` 调用(而不是 down 批 + up 批两次):
|
|
|
|
|
|
/// [Keyboard Input Overview](https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input)
|
|
|
|
|
|
/// 的 Simulating Input 一节明确——单调用注入的一批事件"系统确保不会被其他输入事件
|
|
|
|
|
|
/// 打散"(the system ensures that no other input events intermingle with the
|
|
|
|
|
|
/// simulated events),两次调用之间则可能插入真实键盘/鼠标事件,代理对就有被打散风险。
|
|
|
|
|
|
/// 于是把 N 个 down + N 个 up 拼成一个数组一次提交,顺序仍是全部 down 先于全部 up。
|
2026-10-29 23:29:25 -04:00
|
|
|
|
///
|
|
|
|
|
|
/// 上限由调用方(`common::clamp_text_units`,4096)保证;这里对空输入直接返回,
|
|
|
|
|
|
/// 因为 `SendInput(0)` 会返回 0(失败)且没有任何注入语义。
|
|
|
|
|
|
fn send_text(units: &[u16]) {
|
|
|
|
|
|
if units.is_empty() {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let mk = |scan: u16, up: bool| INPUT {
|
|
|
|
|
|
r#type: INPUT_KEYBOARD,
|
|
|
|
|
|
Anonymous: INPUT_0 {
|
|
|
|
|
|
ki: KEYBDINPUT {
|
|
|
|
|
|
wVk: VIRTUAL_KEY(0), // KEYEVENTF_UNICODE 要求 wVk=0(官方文档)
|
|
|
|
|
|
wScan: scan,
|
|
|
|
|
|
dwFlags: if up {
|
|
|
|
|
|
KEYEVENTF_UNICODE | KEYEVENTF_KEYUP
|
|
|
|
|
|
} else {
|
|
|
|
|
|
KEYEVENTF_UNICODE
|
|
|
|
|
|
},
|
|
|
|
|
|
time: 0,
|
|
|
|
|
|
dwExtraInfo: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
2026-10-29 23:41:54 -04:00
|
|
|
|
// 一次提交:全部 down,紧接全部 up。
|
|
|
|
|
|
let inputs: Vec<INPUT> = units
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|&u| mk(u, false))
|
|
|
|
|
|
.chain(units.iter().map(|&u| mk(u, true)))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let n = unsafe { SendInput(&inputs, std::mem::size_of::<INPUT>() as i32) };
|
2026-10-29 23:29:25 -04:00
|
|
|
|
// 可见失败后果:SendInput 返回「实际插入的事件数」,不等于请求数即失败
|
|
|
|
|
|
// (常见原因:桌面被 Secure UI 锁定、被 BlockInput 拦截)。
|
2026-10-29 23:41:54 -04:00
|
|
|
|
if n as usize != inputs.len() {
|
|
|
|
|
|
eprintln!("[input] SendInput 文本注入不完整: {n}/{}", inputs.len());
|
2026-10-29 23:29:25 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 21:17:55 -04:00
|
|
|
|
pub fn handle(ev: InputEvent) {
|
|
|
|
|
|
match ev {
|
|
|
|
|
|
InputEvent::MouseMove { x, y } => {
|
2026-10-29 22:30:53 -04:00
|
|
|
|
// 归一化坐标系 = **被采集的那个输出**(capture 用 EnumOutputs(0),
|
|
|
|
|
|
// 即主屏;AuthOk 的 width/height 也是它)。
|
|
|
|
|
|
// MOUSEEVENTF_ABSOLUTE 不带 VIRTUALDESK 时,0..=65535 恰好映射到
|
|
|
|
|
|
// 主显示器——与采集空间一致。
|
|
|
|
|
|
// 曾经带 VIRTUALDESK:多显示器机器上注入空间变成整个虚拟桌面
|
|
|
|
|
|
// (比主屏宽),手机画面与指针位置系统性错位(2026-10-30 实测)。
|
|
|
|
|
|
// 单显示器时两者重合,所以桌面端从未暴露此问题。
|
2026-10-22 21:17:55 -04:00
|
|
|
|
send_mouse(
|
2026-10-29 22:30:53 -04:00
|
|
|
|
MOUSEEVENTF_ABSOLUTE.0 | MOUSEEVENTF_MOVE.0,
|
2026-10-22 21:17:55 -04:00
|
|
|
|
x as i32,
|
|
|
|
|
|
y as i32,
|
|
|
|
|
|
0,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
InputEvent::MouseDown(b) => match b {
|
|
|
|
|
|
MouseButton::Left => send_mouse(MOUSEEVENTF_LEFTDOWN, 0, 0, 0),
|
|
|
|
|
|
MouseButton::Right => send_mouse(MOUSEEVENTF_RIGHTDOWN.0, 0, 0, 0),
|
|
|
|
|
|
MouseButton::Middle => send_mouse(MOUSEEVENTF_MIDDLEDOWN.0, 0, 0, 0),
|
|
|
|
|
|
},
|
|
|
|
|
|
InputEvent::MouseUp(b) => match b {
|
|
|
|
|
|
MouseButton::Left => send_mouse(MOUSEEVENTF_LEFTUP, 0, 0, 0),
|
|
|
|
|
|
MouseButton::Right => send_mouse(MOUSEEVENTF_RIGHTUP.0, 0, 0, 0),
|
|
|
|
|
|
MouseButton::Middle => send_mouse(MOUSEEVENTF_MIDDLEUP.0, 0, 0, 0),
|
|
|
|
|
|
},
|
|
|
|
|
|
InputEvent::Wheel { dy } => {
|
|
|
|
|
|
send_mouse(MOUSEEVENTF_WHEEL.0, 0, 0, dy as u32);
|
|
|
|
|
|
}
|
|
|
|
|
|
InputEvent::KeyDown { scancode, extended } => send_key(scancode, extended, false),
|
|
|
|
|
|
InputEvent::KeyUp { scancode, extended } => send_key(scancode, extended, true),
|
2026-10-31 05:34:54 -04:00
|
|
|
|
InputEvent::MouseMoveRel { dx, dy } => send_mouse_rel(dx, dy),
|
2026-10-29 23:29:25 -04:00
|
|
|
|
InputEvent::Text { units } => {
|
|
|
|
|
|
// 上限截断必须可见(与 agent-core::dispatch_input 同口径;Windows 端
|
|
|
|
|
|
// 走自己的 handle(),不经过 agent-core,所以这里同源补齐——R-3 教训)。
|
|
|
|
|
|
let (units, truncated) = common::clamp_text_units(units);
|
|
|
|
|
|
if truncated {
|
|
|
|
|
|
eprintln!(
|
|
|
|
|
|
"[input] 文本超过上限,已截断到 {} 个 code unit",
|
|
|
|
|
|
common::MAX_TEXT_UNITS
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 只打印长度:注入日志不得含用户输入内容(安全红线)
|
|
|
|
|
|
send_text(&units);
|
|
|
|
|
|
}
|
2026-10-22 21:17:55 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|