feat(agent): v3 文本注入用 KEYEVENTF_UNICODE,一次批量提交成对代理项
Windows 侧按官方 KEYBDINPUT 语义实现:置 KEYEVENTF_UNICODE 时 wVk 必须为 0、 wScan 即要送出的字符,系统合成 VK_PACKET 经 TranslateMessage 产出 WM_CHAR。 down 批与 up 批分两次 SendInput,且让同一段文本的 code unit 在输入队列里相邻, 使代理对能被远端合成一个字符(最终由真机 emoji 验收定论)。 agent-core 的 InputInjector::text 用默认实现记日志而非静默忽略:Linux 后端 本轮不做文本注入(X11 需 keysym/XKB 映射,另立循环),但用户对着 Linux 被控端打字不能「什么都没发生还没痕迹」。Windows 端走自己的 handle(), 不经 agent-core,故超限截断日志在两侧同源补齐。 日志只打长度,不打用户输入内容。
This commit is contained in:
parent
38df106fb6
commit
5075888861
@ -48,6 +48,21 @@ pub trait InputInjector: Send + Sync + 'static {
|
||||
fn mouse_button(&self, b: MouseButton, down: bool);
|
||||
fn wheel(&self, dy: i32);
|
||||
fn key(&self, scancode: u16, extended: bool, down: bool);
|
||||
|
||||
/// 协议 v3 的文本注入(UTF-16 code unit)。
|
||||
///
|
||||
/// **默认实现刻意不注入任何东西,但必须留下可见痕迹**:Linux 后端本轮未实现
|
||||
/// 文本注入(X11 需要 keysym/XKB 映射,与 Windows 的 `KEYEVENTF_UNICODE`
|
||||
/// 不同构,另立循环)。全局规则要求异常路径有可见后果——用户对着 Linux 被控端
|
||||
/// 打字时「什么都没发生」不能是静默的,日志必须说明是没实现。
|
||||
///
|
||||
/// 日志纪律:只打印长度,**不打印用户输入内容**。
|
||||
fn text(&self, units: &[u16]) {
|
||||
eprintln!(
|
||||
"[agent-core] 文本注入未实现(忽略 {} 个 UTF-16 code unit)",
|
||||
units.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 把一条协议输入事件分发到注入后端。
|
||||
@ -59,6 +74,17 @@ pub fn dispatch_input(inj: &dyn InputInjector, ev: InputEvent) {
|
||||
InputEvent::Wheel { dy } => inj.wheel(dy),
|
||||
InputEvent::KeyDown { scancode, extended } => inj.key(scancode, extended, true),
|
||||
InputEvent::KeyUp { scancode, extended } => inj.key(scancode, extended, false),
|
||||
InputEvent::Text { units } => {
|
||||
// 上限在前端截断,截断必须可见(否则用户以为长文本已送达)。
|
||||
let (units, truncated) = common::clamp_text_units(units);
|
||||
if truncated {
|
||||
eprintln!(
|
||||
"[agent-core] 文本超过上限,已截断到 {} 个 code unit",
|
||||
common::MAX_TEXT_UNITS
|
||||
);
|
||||
}
|
||||
inj.text(&units);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
//! 输入注入:SendInput 实现(鼠标归一化绝对坐标 + 扫描码键盘)。
|
||||
//! 输入注入: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, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_MIDDLEDOWN,
|
||||
MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
|
||||
MOUSEEVENTF_VIRTUALDESK, MOUSEEVENTF_WHEEL, MOUSEINPUT, VIRTUAL_KEY, MOUSE_EVENT_FLAGS,
|
||||
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,
|
||||
@ -73,6 +74,63 @@ pub fn virtual_desktop() -> (i32, i32, i32, i32) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`。
|
||||
/// - 该 flag「只能与 `KEYEVENTF_KEYUP` 组合」的含义是**不能与 SCANCODE/EXTENDEDKEY 混用**,
|
||||
/// 不是只能发 up 事件——down/up 成对与现有 `send_key` 形态一致。
|
||||
///
|
||||
/// 为什么一次性批量提交(而非逐字符一对 down/up):
|
||||
/// 代理对(U+1F600 = D83D DE00)的两个 code unit 必须在输入队列里**相邻**,
|
||||
/// 远端才会把它们合成成一个字符。逐字符成对(down,up,down,up)会在两个
|
||||
/// down 之间插入一个 up 事件;Windows 对 VK_PACKET 的孤立代理项缓冲的确切
|
||||
/// 规则我无法从文档确定(文档只说明"合成 WM_CHAR",未描述配对窗口),
|
||||
/// 所以这里选择让两个 down 严格相邻的形态,最终由真机 emoji 验收(V7)定论。
|
||||
///
|
||||
/// 上限由调用方(`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,
|
||||
},
|
||||
},
|
||||
};
|
||||
let downs: Vec<INPUT> = units.iter().map(|&u| mk(u, false)).collect();
|
||||
let ups: Vec<INPUT> = units.iter().map(|&u| mk(u, true)).collect();
|
||||
let size = std::mem::size_of::<INPUT>() as i32;
|
||||
// 两批分开提交,且 down 批必须先于 up 批:`SendInput` 按提交顺序投递事件,
|
||||
// 顺序颠倒会让 WM_CHAR 与按键状态错乱。两批之间不再插入其他输入,
|
||||
// 以保证代理对在远端仍相邻。
|
||||
let n_down = unsafe { SendInput(&downs, size) };
|
||||
let n_up = unsafe { SendInput(&ups, size) };
|
||||
// 可见失败后果:SendInput 返回「实际插入的事件数」,不等于请求数即失败
|
||||
// (常见原因:桌面被 Secure UI 锁定、被 BlockInput 拦截)。
|
||||
if n_down as usize != downs.len() || n_up as usize != ups.len() {
|
||||
eprintln!(
|
||||
"[input] SendInput 文本注入不完整: down {n_down}/{} up {n_up}/{}",
|
||||
downs.len(),
|
||||
ups.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(ev: InputEvent) {
|
||||
match ev {
|
||||
InputEvent::MouseMove { x, y } => {
|
||||
@ -105,5 +163,18 @@ pub fn handle(ev: InputEvent) {
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user