diff --git a/agent-core/src/lib.rs b/agent-core/src/lib.rs index ac9551a..d1caef3 100644 --- a/agent-core/src/lib.rs +++ b/agent-core/src/lib.rs @@ -40,6 +40,22 @@ pub trait ScreenCapturer: Send { /// 取出最近一帧,BGRA 紧凑排列(已去掉 row pitch),供 openh264 直接编码。 fn copy_frame(&mut self) -> Result>; + + /// 协议 v5 的光标采样:当前指针在**采集画面内**的物理像素坐标。 + /// 语义与折叠规则(隐藏/离屏 → None)见 `common::VideoFrameHeader::cursor`。 + /// + /// **默认实现刻意返回 None,但必须留下可见痕迹**:Linux 后端本轮未实现光标采样 + /// (X11 `query_pointer` 与 Windows `GetCursorInfo` 不同构,且本机无 X11 无法给 + /// proof——4b/Q4=A 裁决另立循环)。全局规则要求异常路径有可见后果——安卓用户对着 + /// Linux 被控端看不到标记不能是静默的,日志必须说明是没实现。一次性日志防刷屏 + /// (本方法每帧都会被调,最高 60Hz)。 + fn pointer_state(&self) -> Option<(i16, i16)> { + static LOGGED_UNIMPL: AtomicBool = AtomicBool::new(false); + if !LOGGED_UNIMPL.swap(true, Ordering::Relaxed) { + eprintln!("[agent-core] 光标采样后端未实现:帧头 cursor 将恒为 None"); + } + None + } } /// 输入注入后端。实现需可在异步运行时中被多任务共享,故要求 `Send + Sync`。 @@ -176,6 +192,7 @@ fn encode_send( (w, h): (u32, u32), ts_ms: u64, seq: u64, + cursor: Option<(i16, i16)>, tx: &watch::Sender>>, last_frame: &mut Option>, ) -> bool { @@ -189,8 +206,7 @@ fn encode_send( ts_ms, seq, keyframe: h264_contains_idr(&payload), - // v5 光标字段:占位 None,本 crate 的真实采样在下一步接入 - cursor: None, + cursor, }; let frame = encode_video_frame(&header, &payload); if tx.send(Some(frame.clone())).is_err() { @@ -249,7 +265,16 @@ fn capture_session( encoder.force_intra_frame(); } let ts = start.elapsed().as_millis() as u64; - if !encode_send(&mut encoder, &raw, (w, h), ts, seq, tx, &mut last_frame) { + if !encode_send( + &mut encoder, + &raw, + (w, h), + ts, + seq, + cap.pointer_state(), + tx, + &mut last_frame, + ) { return LoopEnd::ChannelClosed; // 会话结束 } seq = seq.wrapping_add(1); @@ -288,7 +313,16 @@ fn capture_session( encoder.force_intra_frame(); let raw = last_raw.as_ref().expect("can_key 已保证"); let ts = start.elapsed().as_millis() as u64; - if !encode_send(&mut encoder, raw, (w, h), ts, seq, tx, &mut last_frame) { + if !encode_send( + &mut encoder, + raw, + (w, h), + ts, + seq, + cap.pointer_state(), + tx, + &mut last_frame, + ) { return LoopEnd::ChannelClosed; } seq = seq.wrapping_add(1); @@ -302,6 +336,9 @@ fn capture_session( Ok((mut hdr, payload)) => { hdr.seq = seq; hdr.ts_ms = start.elapsed().as_millis() as u64; + // v5 与 seq/ts_ms 同理:重发帧的 cursor 若沿用旧值会让标记 + // 冻住(画面静止≠指针静止,触控板用户尤甚) + hdr.cursor = cap.pointer_state(); let frame = encode_video_frame(&hdr, payload); if tx.send(Some(frame)).is_err() { return LoopEnd::ChannelClosed; @@ -692,4 +729,25 @@ mod tests { dispatch_input(&inj, InputEvent::Text { units: vec![] }); assert_eq!(inj.received.lock().unwrap().clone(), vec![0]); } + + /// v5:采集后端未实现 `pointer_state` 时必须返回 None 且不 panic——Linux/X11 端 + /// 本轮的真实形态(4b/Q4=A:默认 None + 一次性日志,实装另立循环)。 + /// 锁的是「默认实现存在且语义为 None」这条契约:若有人把默认实现改成 panic 或 + /// 伪造坐标,这条会红。 + #[test] + fn pointer_state_default_returns_none_without_panic() { + struct NoPointerCapturer; + impl ScreenCapturer for NoPointerCapturer { + fn size(&self) -> (u32, u32) { + (100, 100) + } + fn acquire(&mut self, _timeout_ms: u32) -> Result { + Ok(false) + } + fn copy_frame(&mut self) -> Result> { + Ok(Vec::new()) + } + } + assert_eq!(NoPointerCapturer.pointer_state(), None); + } }