Mar 08, 2026
3 min read
Tauri,

我的赛博女友:使用 Rust Tauri 制作电子桌宠的技术挑战和思考

最近有一个很火的电子桌面宠物应用: AIRI,感兴趣的可以在Github上查看:https://github.com/moeru-ai/airi

它其实是一个基于3D 模型 (Live2D 和 VRM 模型,这两个是专门用于数字人的模型格式,基于glTF)的虚拟桌面宠物,可以和电脑进行交互,拼接入了大模型的功能,让3d模型真正意义上有了智慧的效果。

在桌面平台上,它最开始是一个用 Tauri 开发的 Rust 应用(但是现在它不是了,我不知道具体原因,但是大概原因后面再讲)。

虽然现在 AI 编程让开发门槛大幅度降低了,但是这并不意味着我们不需要思考。

注意:这是在 Linux 下做的实验。

假如你也行

我能实现一个类似的电子桌面宠物吗?

我的想法是, 我可以使用 Tauri 实现一个全屏的、透明置顶、并且需要点击穿透的窗口,来模拟桌面宠物。

重点在于,这个应用的窗口必须是点击穿透的,这样,才不会干扰到用户操作底层的界面。

模型

本质上来说,这其实就是使用前端的 three3D 来渲染3D模型,然后把模型放在一个全屏的窗口里。只是这里我们使用的是比较特殊的模型,即 Live2D 模型或者 VRM 模型。

这些模型格式,是专门为数字人设计的,基于 glTF。因此,它的动作绑定比传统的3D模型要简单得多。比如:

// 左臂自然下垂
    const leftUpperArm = vrm.humanoid.getNormalizedBoneNode('leftUpperArm');
    const leftLowerArm = vrm.humanoid.getNormalizedBoneNode('leftLowerArm');
    const leftHand = vrm.humanoid.getNormalizedBoneNode('leftHand');
    
    // 右臂
    const rightUpperArm = vrm.humanoid.getNormalizedBoneNode('rightUpperArm');
    const rightLowerArm = vrm.humanoid.getNormalizedBoneNode('rightLowerArm');
    const rightHand = vrm.humanoid.getNormalizedBoneNode('rightHand');
    

这是基于传统模型的高度抽象。

我这里使用到的是下面这些资源:

  • three-vrm: 主要用于渲染 VRM 模型
  • vroid hub: VRM 模型资源
  • VRMA: VRMA 是一些封闭好的 VRM 模型动画,这里面有10套常用动画,几乎可以适配所有的 vrm 模型。

效果如下:

1

眼部跟随

不能和主人交互的宠物,它能叫宠物吗? 所以,我希望它的眼睛能跟随鼠标,让3d模型产生一丢丢的交互效果。 实现的思路很简单,就是监听鼠标移动事件,然后计算出鼠标在窗口中的位置,然后把坐标传给模型。实事上,@pixiv/three-vrm 库本身就支持 LookAt 功能,因此我们非常方便集成。

问题的出现

还记得上面的设置吗?我们把应用窗口设置为了点击穿透,也就是 setIgnoreCursorEvents(true)。如果你想控制应用获取鼠标事件,你会发现,这个设置会阻止应用获取鼠标事件。

我们已经拿不到鼠标事件了,这怎么解决呢?

我们肯定会想到一个思路,就是通过 rust 层监听鼠标事件,然后把鼠标事件传递给 js 层,然后 js 层再把鼠标事件传递给应用。

Rust 层可以使用 rdev 或者 Enigo 来获取鼠标事件。

在 rust 层监听鼠标事件:


/// 启动鼠标位置监听线程
fn start_mouse_listener() {
    // 在 Tauri 初始化之前就创建 Enigo 实例
    let enigo = match Enigo::new(&Settings::default()) {
        Ok(e) => Arc::new(Mutex::new(e)),
        Err(e) => {
            eprintln!("无法初始化 Enigo: {:?}", e);
            return;
        }
    };

    // 打印屏幕尺寸
    if let Ok(e) = enigo.lock() {
        if let Ok(display) = e.main_display() {
            println!("屏幕尺寸: {:?}", display);
        }
    }

    println!("开始监听鼠标位置...");

    thread::spawn(move || {
        let mut last_x: i32 = -1;
        let mut last_y: i32 = -1;
        let mut heartbeat = 0u32;

        loop {
            if let Ok(e) = enigo.lock() {
                match e.location() {
                    Ok((x, y)) => {
                        if x != last_x || y != last_y {
                            println!("鼠标位置: x={}, y={}", x, y);
                            MOUSE_X.store(x, Ordering::Relaxed);
                            MOUSE_Y.store(y, Ordering::Relaxed);
                            last_x = x;
                            last_y = y;
                        }
                    }
                    Err(err) => {
                        eprintln!("获取鼠标位置失败: {:?}", err);
                    }
                }
            }

            heartbeat += 1;
            if heartbeat % 60 == 0 {
                println!("[心跳] 鼠标监听线程运行中... (位置: x={}, y={})", last_x, last_y);
            }

            thread::sleep(Duration::from_millis(16)); // ~60fps
        }
    });
}

上面这个代码确实可以监听到鼠标位置。但是,当窗体应用初始化完成时,上面的enigo.lock() 就拿不到 true 了。 因为setIgnoreCursorEvents(true) 是在webview 初始化之后才设置的。这说明,setIgnoreCursorEvents(true) 直接影响到了更底层的地方。通过对 Tauri 的源码分析,我发现 setIgnoreCursorEvents(true) 是 Wry runtime 实现的。

由于我对 Wry 运行时了解不多,所以没有深细研究下去。

但是至少,对于正常开发者来说,这个问题已经难解决了。

看看别人怎么做

既然自己无法解决,那么,我们可以看看别人是怎么做的。通过对 AIRI 项目的源码分析,结果发现 AIRI 项目桌面端已经完成了从 Tauri 到 Electron 的迁移。。。

通过 git commit 历史记录发现, AIRI 项目在使用 Tauri 时,是通过 tauri-plugin-window-pass-through-on-hover 插件实现点击穿透,但是本质上,也是在调用底层的window.set_ignore_cursor_events(enabled) 实现点击穿透。而这个插件,刚好不支持 Linux 系统。

于是我重新在windows 平台试了一下在点击穿透的情况下,在 rust 层获取鼠标位置是否会失效。


thread::spawn(move || {
        let enigo = Enigo::new(&enigo::Settings::default()).unwrap();
        loop {
            let (x, y) = enigo.location().unwrap();
            // 发送鼠标坐标事件到前端
            println!("鼠标位置: x={}, y={}", x, y);
            thread::sleep(Duration::from_millis(50));
        }
    });

答案是可以的。

output

也就是说,上面这个问题,其实是 linux 的问题。目前这个问题由于我不熟悉 wry 和 wayland(x11),暂时找不到可以解决的方案。

麻烦的 linux

虽然我不知道 AIRI 为什么中途切换了Tauri 到 Electron,但是想来,在跨平台的兼容性上, electron 确实比 tauri 更强。

更麻烦的是,目前 Linux 正在处于 x11 被抛弃而 wayland 却完全没有准备好的过滤状态。在开发 linux 应用时,这不是向左或者向右的问题,而是无论向左还是向右,都有可能会出现兼容性的问题。

比如, Tauri 应用在 wayland 模式下运行时,置顶窗口就有可能会失效。 比如, Kde 桌面启动 Tauri的应用时,默认情况下窗体装饰栏的按键响应就会失效,原因是 Tauri 底层使用的是 GTK 。