为什么要做成框架
起因很简单:我想在桌面上养只芙莉莲。
但找了一圈现成的,几乎都是「一个角色一个程序」,换个角色就得换套代码,更别说跨平台了。所以干脆从框架的角度重新想这件事——把「角色」抽象成一个可声明的资源包,渲染、交互、状态机全部解耦,这样新增一个角色只需要丢一份配置进去,主程序一行都不用动。
架构上参考了 BongoCat,它已经验证了 Tauri 做桌宠这条路能走通,我就在它的思路基础上把「多角色 + 可分发」这件事做完整。
它现在长什么样
- 透明、无边框、置顶、跳过任务栏的桌面窗口,按住能拖,
Shift + 右键缩放(20–150%) - 系统托盘常驻,关窗只是隐藏,不会真的退出
- 设置窗分「角色 / 商店 / 外观 / 关于」四页
- 配置持久化 + 跨窗口同步,还会记住窗口位置
- 多角色:内置预设 + 本地目录导入 + 远程商店下载,三条路走同一套资源校验
- macOS / Windows / Linux(X11) 单代码库构建发布
技术栈
| 层 | 技术 |
|---|---|
| 原生层 | Tauri 2 · Rust(窗口与托盘、宠物导入、商店下载、自动更新、公告轮询) |
| 前端 | Vue 3 · TypeScript · Vite · Vue Router · Pinia(@tauri-store/pinia 跨窗口持久化) |
| 渲染 | PixiJS 8 · easy-live2d,抽象出 PetRenderer(已实现 GIF 与 Live2D) |
| 发布 | GitHub Actions 多平台矩阵(macOS arm64/x86_64、Ubuntu、Windows) |
两个我想重点讲的地方
声明式角色状态机
角色不是写死在代码里的,而是一份 pet.json(v1)。UI 这一层只负责上报原始意图——click / doubleClick / mouseEnter / idle 这些;pet.json 里的 capabilities 再把意图映射到角色状态,还支持 cooldownMs、afterMs 这类冷却配置,缺失的能力或状态会静默降级,而不是直接崩掉。
这么做的好处是:加角色 = 加数据,不改逻辑。代价是这个格式得在 TypeScript 和 Rust 两侧各校验一遍——因为角色包可能来自用户导入或远程商店,属于不可信输入,两边都拦一道才放心。
Tauri 2 的跨窗口状态同步
设置窗和主窗口是两个独立的 WebView,状态得同步。我用 @tauri-store/pinia 把 Pinia store 持久化,两个窗口之间自动同步。踩过一个挺隐蔽的坑:这个库约定所有需要持久化的字段都必须出现在初始 state() 里,否则不会被保存——现象就是「设置改了、重启又没了」,查了半天才发现是初始化时漏了字段。现在这条已经写进项目的约定里了。
一点体会
独立把前端、Rust 原生层到发布流程整条链路做完,最大的感受是:桌面端和 Web 端的心智模型差挺多。窗口置顶、托盘、开机自启、自动更新,这些在浏览器里根本不存在的东西,反而是决定桌宠「像不像一个原生应用」的关键。而 Tauri 2 让我能用前端那套熟悉的工具链去碰这些系统能力,还是挺香的。