//! 输入注入:SendInput 实现(鼠标归一化绝对坐标 + 扫描码键盘 + v3 文本注入)。 use common::{InputEvent, MouseButton}; use windows::Win32::UI::Input::KeyboardAndMouse::{ SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, INPUT_MOUSE, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY, 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, }; use windows::Win32::UI::WindowsAndMessaging::{ GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, }; // 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::() as i32); } } 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::() 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), ) } } /// 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`。 /// - 文档原文是 "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` 即可。 /// /// 为什么一次性批量提交(而非逐字符一对 down/up): /// 代理对(U+1F600 = D83D DE00)的两个 code unit 必须在输入队列里**相邻**, /// 远端才会把它们合成成一个字符。逐字符成对(down,up,down,up)会在两个 /// 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。 /// /// 上限由调用方(`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, }, }, }; // 一次提交:全部 down,紧接全部 up。 let inputs: Vec = 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::() as i32) }; // 可见失败后果:SendInput 返回「实际插入的事件数」,不等于请求数即失败 // (常见原因:桌面被 Secure UI 锁定、被 BlockInput 拦截)。 if n as usize != inputs.len() { eprintln!("[input] SendInput 文本注入不完整: {n}/{}", inputs.len()); } } pub fn handle(ev: InputEvent) { match ev { InputEvent::MouseMove { x, y } => { // 归一化坐标系 = **被采集的那个输出**(capture 用 EnumOutputs(0), // 即主屏;AuthOk 的 width/height 也是它)。 // MOUSEEVENTF_ABSOLUTE 不带 VIRTUALDESK 时,0..=65535 恰好映射到 // 主显示器——与采集空间一致。 // 曾经带 VIRTUALDESK:多显示器机器上注入空间变成整个虚拟桌面 // (比主屏宽),手机画面与指针位置系统性错位(2026-10-30 实测)。 // 单显示器时两者重合,所以桌面端从未暴露此问题。 send_mouse( MOUSEEVENTF_ABSOLUTE.0 | MOUSEEVENTF_MOVE.0, 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), 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); } } }