架构
双世界桌面模型——特权 Rust 核心与沙箱 webview,由类型化、能力受限的 IPC 相连。
心智模型是由一条狭窄、类型化边界相连的两个世界。
两个世界
| 世界 | 是什么 | 做什么 |
|---|---|---|
| Rust 核心(特权) | src-tauri/——原生二进制、插件与你的 #[tauri::command] | 触碰 OS:钥匙串、文件系统、通知、更新器、托盘、日志。 |
| webview(沙箱) | src/——系统 webview 中的 React SPA,HashRouter,懒加载路由 | 渲染 UI 并调用核心。无 Node、无 preload 脚本,不直接访问 OS。 |
两者之间的边界是 invoke 加上能力 ACL。UI 代码从不直接触碰 OS——它向核心
请求,而核心只对某个能力已授予的权限作答。这正是相对 Web 模板和 Electron 版的
安全差异:不是 preload 桥,而是每个窗口携带一份枚举好的权限集,Rust 核心会在你的
代码运行前拒绝其外的一切。
一屏看懂 IPC
组件 → tauri.<domain>.<method>() → 生成绑定 → #[tauri::command](Rust)
(或 useTauri('<domain>')) src/lib/bindings.ts src-tauri/src/commands/- 类型由
tauri-specta编译期推导——没有需要同步维护的手写 schema。生成的src/lib/bindings.ts已提交,任何漂移都会使 CI 失败。 - ACL 在命令体运行之前检查权限。
- Zod 保留给设置与表单,而非 IPC 参数。
完整的添加命令流程见 IPC 页。
一个 use-conveyor.ts shim 与 src/lib/conveyor/schemas.ts 从 Electron 移植
中保留下来,仍被部分组件引用。请把 tauri / useTauri 视为真实 API,把 conveyor
层视为移植兼容——其中一些方法是空操作 stub。见 IPC。
中心化配置
所有前端配置在 shared/config.ts 中只读一次;其他地方都不触碰
import.meta.env。它暴露 features、billingConfig、showcaseConfig 与
分析/Sentry 插槽。
开关(features) | Env | 默认 |
|---|---|---|
auth | VITE_AUTH_ENABLED | 开 |
autoUpdate | VITE_AUTO_UPDATE_ENABLED | 开 |
showcase | VITE_SHOWCASE_ENABLED | 开 |
billing | VITE_BILLING_ENABLED | 关 |
一个开关在构建期剥离其整个子系统。关键在于每个子系统只有一个开关——没有
Electron 那样的 MAIN_VITE_* 孪生。原生菜单与托盘用 TypeScript 构建
(src/lib/menu/、src/lib/tray/)并直接读取同一个 features 对象,因此单个
VITE_SHOWCASE_ENABLED 就能触及路由、侧边栏、命令面板以及原生 Template 菜单。
部分模板注释与 README 描述前端在启动时通过 app::set_feature_flags 命令把开关
"推送"给 Rust。该命令并不存在,getNativeFeatureFlags() 也没有任何调用者。
单开关行为是真实的,但原因更简单:菜单/托盘由 JS 构建并直接读取 features。
不存在配置推送。
状态分层
| 层 | 持有 | 支撑 |
|---|---|---|
| zustand store | 会话与权益状态(auth-store、subscription-store) | supabase-js |
| TanStack Query | 服务端数据(todos、订阅读取) | src/lib/query/ |
| 设置存储 | 用户偏好 | tauri-plugin-store + TS 侧版本化迁移 |
启动流程
Rust 入口(main.rs → lib.rs run())以刻意的顺序装配应用:
- single-instance 插件最先——使第二次启动在任何窗口存在前被短路;其回调会 聚焦运行中的窗口,并把新进程的 argv 转发给前端意图分发器。
- 插件——opener、store、dialog、fs、autostart、log、notification、 global-shortcut、os、process、updater、window-state 与 deep-link。
- setup 钩子——仅在 debug 构建中于运行时注册
soar-tauri://协议 (deep_link().register_all()),使深链在tauri dev期间可测;并初始化可选的 Sentry guard。 - 窗口——由
tauri.conf.json以decorations: false(无边框自定义标题栏)创建。 - 前端——
main.tsx校验 Supabase env,再挂载App→OnboardingGate→ 路由。
当 features.auth 开启且 Supabase env 缺失或无效时,main.tsx 渲染
SupabaseConfigErrorScreen,且从不导入 App(否则其 store 导入会构造 Supabase
客户端)。关闭认证时,应用照常挂载且从不触碰该客户端。
第二道防线:CSP
在 ACL 之外,tauri.conf.json 的 app.security 设置了严格的内容安全策略。默认
锚定于 'self';唯一显著放宽的是 connect-src,它允许 IPC 传输以及对 Supabase
和更新源的出站 HTTPS/WSS:
"connect-src 'self' ipc: http://ipc.localhost https: wss:"ipc: 与 http://ipc.localhost 是 webview 触达 Rust 核心的方式;script-src 'self'
(开发环境为 HMR 追加 'unsafe-inline'/'unsafe-eval')意味着应用内无法注入或
加载外来脚本。CSP 与 ACL 一道,把 webview 钉在 self——外部 URL 被强制经由受审计的
opener 收口流出(见 能力)。
