mcpField
T
tauri

tauri

github.com/epicenterhq开发4.8k105 次安装2 周前更新
代码仓库

关于 tauri

面向使用 Tauri 构建桌面应用的开发者,提供 Rust 命令、IPC、权限、路径与文件系统操作的实现规范。重点强调安全边界、生成绑定同步和依据上游资料核验行为。

主要功能

  1. 01Rust 命令与权限管理
  2. 02IPC 与类型绑定规范
  3. 03安全配置与 CSP 建议
  4. 04跨平台路径处理
  5. 05文件系统与原生边界设计

适用场景

  1. 01实现前后端 Tauri 命令
  2. 02调整窗口能力与 CSP
  3. 03生成并核对 TypeScript 绑定
  4. 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 负责强制执行文件系统边界。

评论

支持 Markdown