关于 tauri
面向使用 Tauri 构建桌面应用的开发者,提供 Rust 命令、IPC、权限、路径与文件系统操作的实现规范。重点强调安全边界、生成绑定同步和依据上游资料核验行为。
主要功能
- 01Rust 命令与权限管理
- 02IPC 与类型绑定规范
- 03安全配置与 CSP 建议
- 04跨平台路径处理
- 05文件系统与原生边界设计
适用场景
- 01实现前后端 Tauri 命令
- 02调整窗口能力与 CSP
- 03生成并核对 TypeScript 绑定
- 04处理跨平台文件路径
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill tauri安装后在 Claude Code 中输入/tauri确认已加载。
附带文件references/rust-command-errors.md
详细介绍
Tauri 模式
参考仓库
- Tauri:采用 Rust 后端和 Web 前端的桌面应用框架
上游依据
当 Tauri 命令行为、权限、功能、CSP、资源协议、路径 API、插件文件系统行为或 IPC 语义会影响正确性时,请先查证源码,再依赖记忆。如果 DeepWiki MCP 可用,请针对 tauri-apps/tauri 提出范围明确的问题;如果不可用,或该仓库尚未编入索引,请直接查阅上游源码或官方文档。将 DeepWiki 用作方向参考,然后在修改代码前,针对关键细节查阅本地生成的绑定、已安装的 Rust crate、TypeScript 类型、源码或官方文档加以验证。
对于代码中已清楚显示的仓库本地命令命名和应用特定封装约定,无需查阅 DeepWiki。
命令、权限与安全
- 使用
#[tauri::command]暴露职责明确的 Rust API,通过generate_handler!注册,并为可能失败的操作返回Result<T, E>。 - 在 Rust 侧验证命令输入。TypeScript 调用方不是信任边界。
- 当前 Epicenter 功能按窗口标签授予命令权限。保持命令注册与相关功能授权一致。提议中的 ADR-0402 将用一个主机级常量取代这些设置;该方案尚未实现,添加原生调用时不得假定它已生效。
- 将 CSP、
devCsp、资源协议配置、convertFileSrc、freezePrototype和远程 IPC 视为安全敏感配置。 - 长期存续的 Rust 对象应使用 Tauri 资源,并通过前端
ResourceId标识。不要通过命令响应序列化复杂的长期存续对象。
Webview CSP
绝不能发布 app.security.csp: null(这会完全禁用 CSP)。价值最高的指令是 connect-src:将其限制为你的 API 源和 Tauri 的 IPC,可阻止注入的同源脚本将内存中的机密信息(令牌、密钥)泄露到攻击者的主机。先采用范围尽可能窄的策略,然后只添加应用实际使用的来源,例如资源协议、wasm、workers、媒体或开发服务器来源。设置 csp(生产环境)和 devCsp(开发环境覆盖项,在 tauri dev 期间会替换 csp):
"security": {
// Tauri's tauri-codegen hashes every inline <script> in the built
// frontendDist and injects the hashes, so production script-src does NOT
// need 'unsafe-inline' (a SvelteKit SPA still boots via its hash).
"csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost https://api.example.com wss://api.example.com; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'",
// Dev loads from the Vite server (not the hashed build), so its inline/HMR
// scripts ARE unhashed: devCsp must keep 'unsafe-inline' (+ 'unsafe-eval')
// and add the localhost dev origins.
"devCsp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost http://localhost:5173 ws://localhost:5173 https://api.example.com wss://api.example.com; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; object-src 'none'; base-uri 'self'"
}
规则:始终在 connect-src 中包含 ipc: http://ipc.localhost,否则 invoke() 会失效;只有在确实启用了资源协议(convertFileSrc)时,才列出 asset: / http://asset.localhost;务必对实际运行的 tauri dev 和 release 构建进行冒烟测试,并留意 webview 控制台中的 CSP 违规提示。
类型化 IPC 与生成的绑定
当 Tauri 应用使用 tauri-specta 时,请保持 Rust 命令注册表、生成的 TypeScript 绑定和应用自有的命令接口同步。若命令的错误会被 TypeScript 使用,或需要选择其生成的 Result 如何通过 IPC 传递,请阅读 Rust 命令错误。
- 在
tauri_specta::collect_commands!构建器中注册每个类型化命令。 - 在
tauri_specta::collect_events!中注册前端监听的事件载荷,即使没有命令返回该事件类型也不例外。 - 对于 tauri-specta v2 RC 事件,请在事件类型上使用
#[tauri_specta(event_name = "...")]。除非已安装的宏文档或本地宏源码证实该形式存在,否则不要臆造#[tauri_specta::event(...)]。 - 如果
lib.rs为构建器导入事件和命令载荷类型,请从其所属的 Rust 模块重新导出这些类型。 - 将
bindings.gen.ts视为派生输出。当命令、错误或生成模式发生变化时,请重新生成并检查两类使用方。如果命令只是修正 Rust 的编译形式,没有改变公开的 IPC 契约,请避免大范围的生成内容变动。 - specta 无法为返回原始
tauri::ipc::Response的命令生成绑定,因为其主体不是specta::Type。请通过单独的tauri::generate_handler!路由挂载这些命令,并保留一个简短的手写 TypeScript 封装。
IPC 变更通常需要验证两端:
cargo check --manifest-path apps/epicenter/src-tauri/Cargo.toml
cargo test --manifest-path apps/epicenter/src-tauri/Cargo.toml export_types
如果绑定生成改写了无关部分,请在提交前检查差异。
上下文检测
选择路径 API 前,请先确定代码的执行上下文:
| 上下文 | 位置 | 正确的 API |
|---|---|---|
| Tauri 前端 | apps/*/src/**/*.ts、apps/*/src/**/*.svelte | @tauri-apps/api/path |
| Node.js/Bun 后端 | packages/**/*.ts、CLI 工具 | Node.js path 模块 |
规则:如果代码在浏览器(Tauri webview)中运行,请使用 Tauri 的路径 API。如果代码在 Node.js/Bun 中运行,请使用 Node.js path 模块。
@tauri-apps/api/path 提供的函数
路径操作
| 函数 | 用途 | 示例 |
|---|---|---|
join(...paths) | 使用平台分隔符拼接路径片段 | await join(baseDir, 'workspaces', id) |
dirname(path) | 获取父目录 | await dirname('/foo/bar/file.txt') → /foo/bar |
basename(path, ext?) | 获取文件名,可选择移除扩展名 | await basename('/foo/bar.txt', '.txt') → bar |
extname(path) | 获取文件扩展名 | await extname('file.txt') → .txt |
normalize(path) | 解析 .. 和 . 片段 | await normalize('/foo/bar/../baz') → /foo/baz |
resolve(...paths) | 转换为绝对路径 | await resolve('relative', 'path') |
isAbsolute(path) | 检查路径是否为绝对路径 | await isAbsolute('/foo') → true |
平台常量
| 函数 | 用途 | 返回值 |
|---|---|---|
sep() | 平台路径分隔符 | Windows 上为 \,POSIX 上为 / |
delimiter() | 平台路径分隔符 | Windows 上为 ;,POSIX 上为 : |
在 Tauri v2 中,sep() 和 delimiter() 是同步的。大多数目录和路径操作辅助函数都是异步的,因为它们会调用后端。
基础目录
| 功能 | 用途 |
|---|---|
appLocalDataDir() | 应用的本地数据目录 |
appDataDir() | 应用的漫游数据目录 |
appConfigDir() | 应用的配置目录 |
appCacheDir() | 应用的缓存目录 |
appLogDir() | 应用的日志目录 |
tempDir() | 系统临时目录 |
resourceDir() | 应用的资源目录 |
resolveResource(path) | 解析相对于资源目录的路径 |
模式
构造路径(正确做法)
import { appLocalDataDir, dirname, join } from '@tauri-apps/api/path';
// Join path segments; handles platform separators automatically
const baseDir = await appLocalDataDir();
const filePath = await join(baseDir, 'workspaces', workspaceId, 'data.json');
// Get parent directory; cleaner than manual slicing
const parentDir = await dirname(filePath);
await mkdir(parentDir, { recursive: true });
记录路径(例外情况)
对于人类可读的日志输出,可以使用硬编码的 /,因为它不会用于文件系统操作:
// OK for logging; consistent cross-platform log output
const logPath = pathSegments.join('/');
console.log(`[Persistence] Loading from ${logPath}`);
反模式
切勿:手动拼接字符串
// BAD: Hardcoded separator breaks on Windows
const filePath = baseDir + '/' + 'workspaces' + '/' + id;
// BAD: Template literal with hardcoded separator
const filePath = `${baseDir}/workspaces/${id}`;
// GOOD: Use join()
const filePath = await join(baseDir, 'workspaces', id);
切勿:手动提取父目录
// BAD: Manual slicing is error-prone
const parentSegments = pathSegments.slice(0, -1);
const parentDir = await join(baseDir, ...parentSegments);
// GOOD: Use dirname()
const parentDir = await dirname(filePath);
切勿:在文件系统操作中硬编码分隔符
// BAD: Windows uses backslashes
const configPath = appDir + '/config.json';
// GOOD: Platform-agnostic
const configPath = await join(appDir, 'config.json');
切勿:假设路径格式
// BAD: Splitting on '/' fails on Windows paths
const parts = filePath.split('/');
// GOOD: Use dirname/basename for extraction
const dir = await dirname(filePath);
const file = await basename(filePath);
导入模式
始终从 @tauri-apps/api/path 导入:
import {
appLocalDataDir,
dirname,
join,
basename,
extname,
normalize,
resolve,
sep,
} from '@tauri-apps/api/path';
关于异步操作的说明
大多数 Tauri 路径辅助函数都是异步的,因为它们会通过 IPC 与 Rust 后端通信。假设某个辅助函数会返回 Promise 之前,务必先检查已安装的 TypeScript 类型。目录辅助函数和路径操作辅助函数(如 appLocalDataDir()、join() 和 dirname())是异步的;在 Tauri v2 中,sep() 和 delimiter() 这类简单常量则是同步的。
const baseDir = await appLocalDataDir();
const filePath = await join(baseDir, 'file.txt');
const parent = await dirname(filePath);
const separator = sep();
文件系统操作
使用 @tauri-apps/plugin-fs 进行文件操作,并结合 Tauri 路径 API:
import { appLocalDataDir, dirname, join } from '@tauri-apps/api/path';
import { mkdir, readFile, writeFile } from '@tauri-apps/plugin-fs';
async function saveData(segments: string[], data: Uint8Array) {
const baseDir = await appLocalDataDir();
const filePath = await join(baseDir, ...segments);
// Ensure parent directory exists
const parentDir = await dirname(filePath);
await mkdir(parentDir, { recursive: true });
await writeFile(filePath, data);
}
原生所有权边界
当原生端拥有数据时,优先使用应用拥有的标识符,而不是由前端控制的路径。
- 当 Rust 拥有录音目录时,录音操作应传递录音 ID。
- 模型选择可以传递路径,但 Rust 应对其进行规范化,并拒绝允许的应用数据模型目录之外的值。
- Markdown 导出、下载和临时文件应使用根植于应用拥有目录的专用命令,而非授予宽泛的文件系统权限。
- 移除某项能力、插件或命令时,也应移除描述它的过时文档和 UI。拟议的 ADR-0402 将通过一次模式比较来整合能力验证;当前测试仍会分别覆盖各窗口授权。
前端可以记住用户意图。Rust 负责强制执行文件系统边界。