//! 输入注入: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::{ 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; 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); } } /// 主显示器物理尺寸(像素)。相对移动的 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 { 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::MouseMoveRel { dx, dy } => send_mouse_rel(dx, dy), 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); } } }