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::{
|
|
|
|
|
|
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::<INPUT>() 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::<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-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
|
|
|
|
}
|
|
|
|
|
|
}
|