前言
随着 ChatGPT、Claude 等大语言模型的爆发式发展,AI 应用已经深入到我们工作生活的方方面面。但现有的 AI 工具大多是 Web 端或者需要联网使用,对于一些希望在本地进行更深度定制化使用的用户来说,还缺少一个真正意义上的「AI 工作台」。
基于这个出发点,我开发了智能工具箱(Smart Toolbox) —— 一款基于 Tauri 2.x 构建的桌面 AI 助手,支持多模型接入、自定义技能、工作流编排等功能。
坦白说,选择 Tauri + Rust 这个技术栈,很大程度上是出于对新技术的好奇和探索欲。在此之前,我有过 Electron 开发经验,深知其生态成熟、开发效率高的优势,但也一直被其臃肿的包体积和高内存占用所困扰。当了解到 Tauri 可以用 Rust 替代 Node.js 后端,同时保持前端开发体验时,我立刻产生了浓厚的兴趣——这正好给了我一个学习 Rust、探索现代桌面开发新范式的机会。
此外,项目的设计灵感也部分来源于当前流行的 AI 编程助手生态,特别是 Claude Code、Cursor 等工具所采用的 Agent Skills 规范。这些工具通过标准化的技能定义(SKILL.md 文件)实现了技能的可复用和可共享,这给了我很大的启发。我希望在智能工具箱中不仅兼容这种规范,还能在此基础上进行扩展,提供更强大的功能。
本文将详细介绍这款应用的技术架构和核心实现,同时也会分享从 Electron 转向 Tauri 的开发体验对比,以及 Agent Skills 兼容层的设计与实现。
典型使用场景
智能工具箱作为一款桌面 AI 助手,适用于多种使用场景:
场景1:内容创作者
用户需求:需要快速生成高质量文章,并发布到多个平台
使用流程:
- 用户输入:"帮我写一篇关于 Rust 并发编程的技术文章,1500字左右"
- 意图识别引擎识别到需要调用"文章生成"技能
- 技能执行器加载技能配置,调用 LLM 生成文章
- 用户预览并调整文章内容
- 使用"多平台发布"工作流,一键发布到掘金、公众号等平台
收益:从构思到发布,整个过程仅需 10-15 分钟,相比传统方式节省 80% 时间
场景2:开发者助手
用户需求:需要快速生成代码、处理文件、执行自动化任务
使用流程:
- 用户输入:"帮我生成一个 React 组件,包含表单验证功能"
- 系统调用"代码生成"技能,生成完整的组件代码
- 用户可以进一步要求:"添加 TypeScript 类型定义"
- 系统自动更新代码,添加类型定义
- 用户可以直接复制代码到项目中使用
收益:提高开发效率,减少重复性工作
场景3:数据分析师
用户需求:需要处理 Excel 数据,生成可视化报告
使用流程:
- 用户上传 Excel 文件
- 输入:"帮我分析这个销售数据,生成月度趋势图"
- 系统调用"数据处理"技能(Script 类型),执行 Python 脚本
- 生成数据可视化图表
- 用户可以导出报告或进一步分析
收益:无需编写代码,通过自然语言完成数据分析
场景4:知识工作者
用户需求:需要整理会议记录、生成周报、优化文档
使用流程:
- 用户粘贴会议记录文本
- 输入:"帮我整理成结构化的会议纪要"
- 系统调用"文档优化"技能,生成格式化的会议纪要
- 用户可以进一步要求:"提取待办事项"
- 系统生成待办事项列表
收益:提高文档处理效率,减少格式化工作
一、技术选型:为什么选择 Tauri + Rust?
1.1 跨平台桌面开发的选择
在开始开发前,我对比了主流的桌面跨平台方案:
| 方案 | 优势 | 劣势 | 包体积 |
|---|---|---|---|
| Electron | 生态成熟,开发效率高 | 内存占用大,包体积大 | 150MB+ |
| Tauri | 包体积小,性能好,安全性高 | 生态相对年轻 | 10-20MB |
| Flutter Desktop | 一套代码多端运行 | 桌面端生态不成熟 | 20-30MB |
| Qt | 成熟稳定 | 开发成本高,界面开发繁琐 | 50MB+ |
最终选择 Tauri 2.x 的原因:
- 极小的包体积:最终打包后仅 15MB 左右,相比 Electron 动辄 150MB+ 的体积,用户体验更好
- Rust 后端:高性能、内存安全,非常适合处理加密、文件操作等底层任务
- 安全性:默认最小权限原则,IPC 通信有严格的命令白名单机制
- Tauri 2.x 新特性:更好的插件系统、移动端支持预览、更灵活的权限控制
- 学习新技术的机会:Rust 的所有权模型、类型系统让我对系统编程有了更深的理解,这种学习过程本身就是一种收获
1.2 Agent Skills 生态兼容性
在开始技术选型之前,我想先谈谈项目的一个重要设计目标:兼容 Agent Skills 生态。
Agent Skills 是 Claude Code、Cursor、Windsurf 等 AI 编程助手所采用的技能定义规范。它通过标准化的 SKILL.md 文件格式,让技能可以在不同的 AI 工具之间共享和复用。
Agent Skills 标准格式:
.claude/skills/my-skill/
├── SKILL.md # 技能定义(YAML frontmatter + Markdown)
├── scripts/ # 可选:脚本文件
│ └── main.py
├── examples/ # 可选:使用示例
│ └── example.md
└── references/ # 可选:参考文档
└── reference.md
智能工具箱的兼容策略:
- 双向转换:支持将 Agent Skills 格式转换为内部 DSL,反之亦然
- 功能扩展:在兼容的基础上,增加了 Workflow 编排、Token 优化等高级功能
- 生态互通:用户可以直接导入 Claude Code 的技能,也可以将智能工具箱的技能导出供其他工具使用
这种设计让智能工具箱既能享受 Agent Skills 生态的丰富资源,又能提供更强大的本地能力。
1.3 技术栈全景
┌──────────────────────────────────────────────────────────────────┐
│ 前端 (Frontend) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ React 18 + TypeScript + Tailwind CSS + shadcn/ui │ │
│ │ Zustand (状态管理) + React Router (路由) │ │
│ └─────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────┤
│ Tauri IPC Bridge │
│ (基于 Tauri Commands) │
├──────────────────────────────────────────────────────────────────┤
│ 后端 (Backend) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Tauri 2.x + Rust │ │
│ │ ├── Commands Layer (命令层) │ │
│ │ ├── Services Layer (服务层) │ │
│ │ ├── LLM Providers (多模型适配器) │ │
│ │ └── SQLite Database (本地存储) │ │
│ └─────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
二、核心架构设计
2.1 整体架构图
智能工具箱架构
├── 📱 前端 UI 层
│ ├── pages/ # 页面组件
│ │ ├── ChatPage # 对话页面
│ │ ├── SkillsPage # 技能库页面
│ │ ├── WorkflowPage # 工作流页面
│ │ └── SettingsPage # 设置页面
│ ├── components/ # 通用组件
│ │ ├── ui/ # shadcn/ui 基础组件
│ │ ├── wizard/ # 技能生成向导组件
│ │ └── platform/ # 平台发布组件
│ ├── stores/ # Zustand 状态管理
│ │ ├── chatStore # 对话状态
│ │ ├── skillStore # 技能状态
│ │ └── configStore # 配置状态
│ └── types/ # TypeScript 类型定义
│
└── ⚙️ 后端 Rust 层
├── commands/ # Tauri 命令层
│ ├── chat.rs # 对话相关命令
│ ├── skills.rs # 技能管理命令
│ ├── workflow.rs # 工作流命令
│ └── config.rs # 配置命令
├── services/ # 核心服务层
│ ├── skill_executor # 技能执行器
│ ├── skill_wizard # 技能生成向导
│ ├── intent_recognizer # 意图识别
│ ├── token_optimizer # Token 优化
│ └── platform_publisher # 平台发布
├── llm/ # LLM 多模型适配
│ ├── providers.rs # 模型提供商适配
│ └── types.rs # LLM 类型定义
├── workflow/ # 工作流引擎
│ ├── engine.rs # 执行引擎
│ └── types.rs # 工作流类型
└── database/ # 数据库层
├── models.rs # 数据模型
└── migrations.rs # 数据库迁移
2.2 数据流设计
以一次典型的对话流程为例:
用户输入 "帮我写一篇关于 Rust 的文章"
│
▼
┌─────────────────┐
│ ChatPage.tsx │ ← 前端捕获用户输入
└────────┬────────┘
│ invoke('send_message', {...})
▼
┌─────────────────┐
│ chat.rs │ ← Tauri 命令层
│ send_message() │
└────────┬────────┘
│
▼
┌─────────────────┐
│ IntentRecognizer│ ← 意图识别服务
│ analyze_intent()│ 判断用户想做什么
└────────┬────────┘
│ 识别到:调用技能 "article-writer"
▼
┌─────────────────┐
│ SkillExecutor │ ← 技能执行器
│ execute() │ 加载技能配置,准备提示词
└────────┬────────┘
│
▼
┌─────────────────┐
│ LLM Provider │ ← 调用大语言模型
│ chat_stream() │ 支持 OpenAI/Claude/DeepSeek 等
└────────┬────────┘
│ SSE 流式响应
▼
┌─────────────────┐
│ TokenOptimizer │ ← Token 优化
│ record_usage() │ 记录使用量,优化会话
└────────┬────────┘
│
▼
┌─────────────────┐
│ ChatPage.tsx │ ← 前端渲染结果
│ 实时显示流式输出│
└─────────────────┘
三、核心模块实现解析
3.1 多模型 LLM 适配器
为了支持多种大语言模型,我设计了一套统一的适配器接口:
// src-tauri/src/llm/providers.rs
/// LLM 提供商枚举
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LLMProvider {
OpenAI,
Claude,
DeepSeek,
AI360,
Ollama,
}
/// 统一的 LLM 客户端
pub struct LLMClient {
provider: LLMProvider,
api_key: String,
base_url: Option<String>,
model: String,
}
impl LLMClient {
/// 流式对话接口
pub async fn chat_stream(
&self,
messages: Vec<ChatMessage>,
options: ChatOptions,
) -> Result<impl Stream<Item = Result<String, AppError>>, AppError> {
match self.provider {
LLMProvider::OpenAI => self.openai_stream(messages, options).await,
LLMProvider::Claude => self.claude_stream(messages, options).await,
LLMProvider::DeepSeek => self.deepseek_stream(messages, options).await,
LLMProvider::AI360 => self.ai360_stream(messages, options).await,
LLMProvider::Ollama => self.ollama_stream(messages, options).await,
}
}
/// OpenAI 兼容接口实现
async fn openai_stream(
&self,
messages: Vec<ChatMessage>,
options: ChatOptions,
) -> Result<impl Stream<Item = Result<String, AppError>>, AppError> {
let url = format!(
"{}/chat/completions",
self.base_url.as_deref().unwrap_or("https://api.openai.com/v1")
);
let payload = json!({
"model": self.model,
"messages": messages,
"stream": true,
"temperature": options.temperature.unwrap_or(0.7),
"max_tokens": options.max_tokens,
});
let response = reqwest::Client::new()
.post(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await?;
// 处理 SSE 流式响应
let stream = response.bytes_stream().map(|chunk| {
// 解析 SSE 数据...
});
Ok(stream)
}
}
设计亮点:
- 使用枚举统一管理所有支持的模型提供商
- 流式接口返回
Stream,支持实时渲染 - DeepSeek、360AI 等国产模型兼容 OpenAI 接口格式,代码复用率高
3.2 技能系统设计
技能是智能工具箱的核心概念,一个技能可以理解为「一个可复用的 AI 任务模板」。
技能数据结构
// src-tauri/src/skills/mod.rs
/// 技能类型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SkillType {
Prompt, // 提示词技能:纯 LLM 调用
Script, // 脚本技能:执行代码
Workflow, // 工作流技能:多步骤编排
}
/// 技能定义
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Skill {
pub id: String,
pub name: String,
pub display_name: String,
pub description: String,
pub skill_type: SkillType,
pub category: SkillCategory,
pub triggers: Vec<String>, // 触发词
pub icon: String,
pub enabled: bool,
pub prompt_config: Option<PromptConfig>,
pub script_config: Option<ScriptConfig>,
pub workflow_config: Option<WorkflowConfig>,
}
/// 提示词配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptConfig {
pub system: String, // 系统提示词(完整版)
pub system_compact: String, // 系统提示词(精简版,省 Token)
pub variables: Vec<SkillVariable>,
pub output_format: Option<String>,
}
/// 技能变量
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillVariable {
pub name: String,
pub var_type: String, // string / number / boolean / array
pub description: String,
pub required: bool,
pub default: Option<Value>,
}
技能执行器
// src-tauri/src/services/skill_executor.rs
pub struct SkillExecutor {
llm_client: LLMClient,
db: Database,
}
impl SkillExecutor {
/// 执行技能
pub async fn execute(
&self,
skill: &Skill,
context: &SkillContext,
) -> Result<SkillResult, AppError> {
match skill.skill_type {
SkillType::Prompt => self.execute_prompt(skill, context).await,
SkillType::Script => self.execute_script(skill, context).await,
SkillType::Workflow => self.execute_workflow(skill, context).await,
}
}
/// 执行提示词技能
async fn execute_prompt(
&self,
skill: &Skill,
context: &SkillContext,
) -> Result<SkillResult, AppError> {
let config = skill.prompt_config.as_ref()
.ok_or_else(|| AppError::Skill("技能配置缺失".to_string()))?;
// 1. 根据 Token 优化策略选择提示词版本
let system_prompt = if context.use_compact_prompt {
&config.system_compact
} else {
&config.system
};
// 2. 变量替换
let processed_prompt = self.replace_variables(system_prompt, &context.variables)?;
// 3. 构建消息列表
let messages = vec![
ChatMessage { role: "system".to_string(), content: processed_prompt },
ChatMessage { role: "user".to_string(), content: context.user_input.clone() },
];
// 4. 调用 LLM
let response = self.llm_client.chat_stream(messages, ChatOptions::default()).await?;
Ok(SkillResult {
success: true,
output: response,
token_used: 0, // 实际会在流结束后统计
})
}
}
3.3 意图识别引擎
当用户输入一段话后,系统需要判断用户的意图,决定是直接对话、调用技能、还是执行其他操作。
// src-tauri/src/services/intent_recognizer.rs
/// 用户意图类型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UserIntent {
/// 调用已有技能
InvokeSkill { skill_id: String, confidence: f32 },
/// 创建新技能
CreateSkill { description: String },
/// 管理技能(查看、编辑、删除)
ManageSkill { action: String },
/// 执行工作流
RunWorkflow { workflow_id: String },
/// 普通对话
Chat,
}
pub struct IntentRecognizer {
skills: Vec<Skill>,
}
impl IntentRecognizer {
/// 分析用户意图
pub fn analyze(&self, input: &str) -> UserIntent {
// 1. 先尝试关键词匹配(快速路径)
if let Some(intent) = self.keyword_match(input) {
return intent;
}
// 2. 触发词匹配(遍历所有技能的 triggers)
for skill in &self.skills {
for trigger in &skill.triggers {
if input.contains(trigger) {
return UserIntent::InvokeSkill {
skill_id: skill.id.clone(),
confidence: 0.9,
};
}
}
}
// 3. 模糊匹配(使用编辑距离算法)
if let Some((skill_id, score)) = self.fuzzy_match(input) {
if score > 0.7 {
return UserIntent::InvokeSkill {
skill_id,
confidence: score,
};
}
}
// 4. 默认为普通对话
UserIntent::Chat
}
/// 关键词匹配
fn keyword_match(&self, input: &str) -> Option<UserIntent> {
let create_keywords = ["创建技能", "新建技能", "生成技能", "做一个技能"];
let manage_keywords = ["查看技能", "编辑技能", "删除技能", "技能列表"];
for keyword in create_keywords {
if input.contains(keyword) {
return Some(UserIntent::CreateSkill {
description: input.to_string(),
});
}
}
for keyword in manage_keywords {
if input.contains(keyword) {
return Some(UserIntent::ManageSkill {
action: keyword.to_string(),
});
}
}
None
}
}
3.4 Token 优化策略
调用 LLM 需要消耗 Token,如何在保证效果的前提下节省 Token 是一个重要课题。
// src-tauri/src/services/token_optimizer.rs
/// Token 优化器
pub struct TokenOptimizer {
db: Database,
cache: HashMap<String, SessionCache>,
}
/// Prompt 分级策略
#[derive(Debug, Clone)]
pub enum PromptGrade {
Full, // 完整版:首次调用、复杂任务
Compact, // 精简版:连续调用、简单任务
}
impl TokenOptimizer {
/// 决定使用哪个版本的 Prompt
pub fn decide_prompt_grade(
&self,
skill_id: &str,
context: &CallContext,
) -> PromptGrade {
// 策略 1:连续调用同一技能,使用精简版
if let Some(cache) = self.cache.get(skill_id) {
if cache.is_recent() && cache.call_count > 1 {
return PromptGrade::Compact;
}
}
// 策略 2:用户主动选择精简模式
if context.prefer_compact {
return PromptGrade::Compact;
}
// 策略 3:简单输入使用精简版
if context.user_input.len() < 50 {
return PromptGrade::Compact;
}
PromptGrade::Full
}
/// 会话复用:连续调用同一技能时复用上下文
pub fn get_or_create_session(
&mut self,
skill_id: &str,
ttl_seconds: u64,
) -> &mut SessionCache {
self.cache.entry(skill_id.to_string())
.or_insert_with(|| SessionCache::new(ttl_seconds))
}
/// 记录 Token 使用量
pub async fn record_usage(
&self,
skill_id: &str,
input_tokens: u32,
output_tokens: u32,
) -> Result<(), AppError> {
self.db.execute(
"INSERT INTO skill_usage_stats (skill_id, input_tokens, output_tokens, created_at)
VALUES (?, ?, ?, ?)",
params![skill_id, input_tokens, output_tokens, Utc::now()],
).await?;
Ok(())
}
}
3.5 工作流引擎
工作流允许用户将多个技能/操作串联起来,形成自动化流程。
// src-tauri/src/workflow/engine.rs
/// 工作流执行引擎
pub struct WorkflowEngine {
skill_executor: SkillExecutor,
http_client: HttpClient,
}
/// 工作流节点类型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WorkflowNodeType {
Start,
End,
Skill, // 调用技能
Condition, // 条件分支
Loop, // 循环
SetVariable, // 设置变量
HttpRequest, // HTTP 请求
Delay, // 延时
}
impl WorkflowEngine {
/// 执行工作流
pub async fn execute(
&mut self,
workflow: &Workflow,
initial_input: Value,
) -> Result<WorkflowResult, AppError> {
let mut context = WorkflowContext::new(initial_input);
// 找到起始节点
let start_node = workflow.nodes.iter()
.find(|n| n.node_type == WorkflowNodeType::Start)
.ok_or_else(|| AppError::Workflow("缺少起始节点".to_string()))?;
// 使用栈迭代执行,避免递归
let mut stack = vec![start_node.id.clone()];
while let Some(node_id) = stack.pop() {
let node = workflow.get_node(&node_id)?;
// 执行节点
let result = self.execute_node(node, &mut context).await?;
// 根据结果决定下一个节点
match node.node_type {
WorkflowNodeType::End => break,
WorkflowNodeType::Condition => {
// 条件节点:根据结果选择分支
let next = if result.as_bool().unwrap_or(false) {
&node.data.true_branch
} else {
&node.data.false_branch
};
if let Some(next_id) = next {
stack.push(next_id.clone());
}
}
_ => {
// 普通节点:找到下一个连接的节点
if let Some(edge) = workflow.edges.iter().find(|e| e.source == node_id) {
stack.push(edge.target.clone());
}
}
}
}
Ok(WorkflowResult {
success: true,
outputs: context.outputs,
variables: context.variables,
})
}
/// 执行单个节点
async fn execute_node(
&self,
node: &WorkflowNode,
context: &mut WorkflowContext,
) -> Result<Value, AppError> {
match node.node_type {
WorkflowNodeType::Skill => {
let skill_id = node.data.skill_id.as_ref()
.ok_or_else(|| AppError::Workflow("技能节点缺少 skill_id".to_string()))?;
// 执行技能并获取结果
let result = self.skill_executor.execute_by_id(skill_id, context).await?;
// 将结果存入上下文,供后续节点使用
context.set_node_output(&node.id, result.clone());
Ok(result)
}
WorkflowNodeType::HttpRequest => {
let config = &node.data.http_config;
let response = self.http_client.request(config).await?;
context.set_node_output(&node.id, response.clone());
Ok(response)
}
WorkflowNodeType::SetVariable => {
let var_name = &node.data.variable_name;
let value = self.resolve_value(&node.data.value, context)?;
context.variables.insert(var_name.clone(), value.clone());
Ok(value)
}
WorkflowNodeType::Delay => {
let ms = node.data.delay_ms.unwrap_or(1000);
tokio::time::sleep(Duration::from_millis(ms)).await;
Ok(json!({"delayed_ms": ms}))
}
_ => Ok(json!({})),
}
}
}
3.6 API Key 安全存储
API Key 是敏感信息,需要加密存储:
// src-tauri/src/services/crypto.rs
use aes_gcm::{Aes256Gcm, Key, Nonce};
use aes_gcm::aead::{Aead, NewAead};
/// 加密服务
pub struct CryptoService {
key: Key<Aes256Gcm>,
}
impl CryptoService {
/// 从设备指纹派生密钥
pub fn from_device_fingerprint() -> Self {
// 组合多个设备特征生成唯一指纹
let fingerprint = format!(
"{}-{}-{}",
whoami::hostname(),
whoami::username(),
machine_uid::get().unwrap_or_default()
);
// 使用 PBKDF2 派生 256 位密钥
let salt = b"smart-toolbox-salt";
let mut key = [0u8; 32];
pbkdf2::pbkdf2::<Hmac<Sha256>>(
fingerprint.as_bytes(),
salt,
100_000,
&mut key,
);
Self { key: Key::from_slice(&key).clone() }
}
/// 加密 API Key
pub fn encrypt(&self, plaintext: &str) -> Result<String, AppError> {
let cipher = Aes256Gcm::new(&self.key);
let nonce = Nonce::from_slice(b"unique nonce"); // 实际应使用随机 nonce
let ciphertext = cipher.encrypt(nonce, plaintext.as_bytes())
.map_err(|e| AppError::Crypto(e.to_string()))?;
Ok(base64::encode(&ciphertext))
}
/// 解密 API Key
pub fn decrypt(&self, ciphertext: &str) -> Result<String, AppError> {
let cipher = Aes256Gcm::new(&self.key);
let nonce = Nonce::from_slice(b"unique nonce");
let ciphertext_bytes = base64::decode(ciphertext)
.map_err(|e| AppError::Crypto(e.to_string()))?;
let plaintext = cipher.decrypt(nonce, ciphertext_bytes.as_ref())
.map_err(|e| AppError::Crypto(e.to_string()))?;
String::from_utf8(plaintext)
.map_err(|e| AppError::Crypto(e.to_string()))
}
}
3.7 Agent Skills 兼容层设计
为了与 Agent Skills 生态互通,我设计了一个专门的兼容层,实现双向转换功能。
3.7.1 Agent Skills 格式解析
Agent Skills 使用 SKILL.md 文件定义技能,格式如下:
---
name: article-writer
description: 根据主题生成高质量文章
version: 1.0.0
author: Your Name
tags: [writing, content]
---
# Article Writer Skill
## Description
This skill generates high-quality articles based on a given topic.
## When to Use This Skill
- You need to write a blog post
- You want to create content for social media
- You need to draft an article quickly
## How to Use
Simply provide a topic and optionally specify the style and length.
## Examples
### Example 1: Tech Article
**Input**: "Write a 1500-word technical article about Rust"
**Output**: [Generated article content]
3.7.2 导入转换实现
// src-tauri/src/commands/agent_skills.rs
use std::path::Path;
use serde_yaml::Value;
use regex::Regex;
/// Agent Skills 前置元数据
#[derive(Debug, Deserialize)]
struct AgentSkillFrontmatter {
name: String,
description: String,
#[serde(default)]
version: String,
#[serde(default)]
author: String,
#[serde(default)]
tags: Vec<String>,
}
/// 解析 SKILL.md 文件
pub fn parse_skill_md(path: &Path) -> Result<Skill, AppError> {
let content = std::fs::read_to_string(path)?;
// 提取 YAML frontmatter
let frontmatter_re = Regex::new(r"^---\n(.*?)\n---").unwrap();
let caps = frontmatter_re.captures(&content)
.ok_or_else(|| AppError::Parse("缺少 YAML frontmatter".to_string()))?;
let yaml_str = &caps[1];
let frontmatter: AgentSkillFrontmatter = serde_yaml::from_str(yaml_str)
.map_err(|e| AppError::Parse(format!("YAML 解析失败: {}", e)))?;
// 提取 Markdown 正文
let markdown_body = frontmatter_re.replace(&content, "").to_string();
// 判断技能类型
let skill_dir = path.parent().unwrap();
let scripts_dir = skill_dir.join("scripts");
let skill_type = if scripts_dir.exists() {
SkillType::Script
} else {
SkillType::Prompt
};
// 转换为内部 Skill 格式
Ok(Skill {
id: format!("agent-skills-{}", frontmatter.name),
name: frontmatter.name.clone(),
display_name: frontmatter.name,
description: frontmatter.description,
skill_type,
category: infer_category(&frontmatter.tags),
triggers: extract_triggers(&markdown_body),
icon: "📦".to_string(),
enabled: true,
prompt_config: if skill_type == SkillType::Prompt {
Some(PromptConfig {
system: markdown_body.clone(),
system_compact: compact_prompt(&markdown_body),
variables: extract_variables(&markdown_body),
output_format: None,
})
} else {
None
},
script_config: if skill_type == SkillType::Script {
Some(ScriptConfig {
language: detect_script_language(&scripts_dir)?,
code: load_script_code(&scripts_dir)?,
})
} else {
None
},
workflow_config: None,
})
}
/// 从目录导入所有 Agent Skills
#[tauri::command]
pub async fn import_agent_skills() -> Result<Vec<Skill>, AppError> {
let skills_dir = Path::new(".claude/skills");
let mut skills = Vec::new();
if !skills_dir.exists() {
return Ok(skills);
}
for entry in std::fs::read_dir(skills_dir)? {
let entry = entry?;
let skill_md = entry.path().join("SKILL.md");
if skill_md.exists() {
if let Ok(skill) = parse_skill_md(&skill_md) {
skills.push(skill);
}
}
}
Ok(skills)
}
3.7.3 导出转换实现
/// 将内部 Skill 导出为 Agent Skills 格式
#[tauri::command]
pub async fn export_to_agent_skills(skill: &Skill) -> Result<String, AppError> {
let frontmatter = format!(
r#"---
name: {}
description: {}
version: {}
author: {}
tags: [{}]
---
"#,
skill.name,
skill.description,
"1.0.0",
"Smart Toolbox",
skill.category.to_lowercase()
);
let mut content = frontmatter;
// 添加技能描述
content.push_str(&format!("# {}\n\n", skill.display_name));
content.push_str(&format!("## Description\n{}\n\n", skill.description));
// 添加触发词
if !skill.triggers.is_empty() {
content.push_str("## Triggers\n");
for trigger in &skill.triggers {
content.push_str(&format!("- {}\n", trigger));
}
content.push_str("\n");
}
// 添加使用说明
content.push_str("## How to Use\n");
content.push_str("This skill can be triggered by the keywords listed above.\n\n");
// 添加 Prompt 内容
if let Some(config) = &skill.prompt_config {
content.push_str("## System Prompt\n\n");
content.push_str(&config.system);
}
Ok(content)
}
/// 导出技能到文件系统
#[tauri::command]
pub async fn export_skill_to_file(skill: &Skill, output_dir: String) -> Result<String, AppError> {
let skill_dir = Path::new(&output_dir).join(&skill.name);
std::fs::create_dir_all(&skill_dir)?;
// 写入 SKILL.md
let skill_md = export_to_agent_skills(skill).await?;
std::fs::write(skill_dir.join("SKILL.md"), skill_md)?;
// 如果是 Script 类型,复制脚本文件
if let Some(config) = &skill.script_config {
let scripts_dir = skill_dir.join("scripts");
std::fs::create_dir_all(&scripts_dir)?;
let script_file = match config.language.as_str() {
"python" => "main.py",
"javascript" => "main.js",
"typescript" => "main.ts",
_ => "script.txt",
};
std::fs::write(scripts_dir.join(script_file), &config.code)?;
}
Ok(skill_dir.to_string_lossy().to_string())
}
3.7.4 智能推断与增强
在转换过程中,系统会进行智能推断和功能增强:
/// 从标签推断技能分类
fn infer_category(tags: &[String]) -> SkillCategory {
let tag_str = tags.join(" ").to_lowercase();
if tag_str.contains("writing") || tag_str.contains("content") {
SkillCategory::Content
} else if tag_str.contains("code") || tag_str.contains("dev") {
SkillCategory::Development
} else if tag_str.contains("data") || tag_str.contains("analysis") {
SkillCategory::Data
} else if tag_str.contains("publish") || tag_str.contains("social") {
SkillCategory::Publish
} else {
SkillCategory::General
}
}
/// 从 Markdown 内容中提取触发词
fn extract_triggers(content: &str) -> Vec<String> {
let mut triggers = Vec::new();
// 提取标题中的关键词
let title_re = Regex::new(r"^#+\s*(.+)$").unwrap();
for caps in title_re.captures_iter(content) {
let title = &caps[1];
if title.len() < 20 {
triggers.push(title.to_string());
}
}
// 提取列表项中的关键词
let list_re = Regex::new(r"^\s*[-*]\s*(.+)$").unwrap();
for caps in list_re.captures_iter(content) {
let item = &caps[1];
if item.len() < 30 {
triggers.push(item.to_string());
}
}
triggers.dedup();
triggers
}
/// 生成精简版 Prompt(Token 优化)
fn compact_prompt(full_prompt: &str) -> String {
// 移除多余的空行
let compressed = Regex::new(r"\n{3,}")
.unwrap()
.replace_all(full_prompt, "\n\n");
// 移除注释
let without_comments = Regex::new(r"<!--.*?-->")
.unwrap()
.replace_all(&compressed, "");
without_comments.to_string()
}
3.7.5 使用示例
导入 Agent Skills:
// 前端调用
const skills = await invoke<Skill[]>('import_agent_skills');
// 显示导入的技能
skills.forEach(skill => {
console.log(`导入技能: ${skill.display_name}`);
});
导出为 Agent Skills:
// 导出单个技能
const outputPath = await invoke<string>('export_skill_to_file', {
skill: selectedSkill,
outputDir: '/path/to/.claude/skills'
});
console.log(`技能已导出到: ${outputPath}`);
设计亮点:
- 无损转换:保留原始技能的所有信息,包括描述、示例、参考文档
- 智能推断:自动推断技能分类、触发词、变量定义
- 功能增强:导入的技能自动获得 Token 优化、工作流编排等高级功能
- 生态互通:用户可以在 Claude Code、Cursor 等工具中使用智能工具箱的技能
四、前端架构设计
4.1 状态管理:Zustand
选择 Zustand 而非 Redux 的原因:
- 更简洁的 API,学习成本低
- 内置支持持久化、中间件
- 与 React 18 的 Concurrent Mode 兼容性好
// src/stores/chatStore.ts
import { create } from 'zustand';
import { invoke } from '@tauri-apps/api/core';
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: number;
}
interface ChatState {
messages: Message[];
isLoading: boolean;
currentConversationId: string | null;
// Actions
sendMessage: (content: string) => Promise<void>;
clearMessages: () => void;
loadConversation: (id: string) => Promise<void>;
}
export const useChatStore = create<ChatState>((set, get) => ({
messages: [],
isLoading: false,
currentConversationId: null,
sendMessage: async (content: string) => {
const userMessage: Message = {
id: crypto.randomUUID(),
role: 'user',
content,
timestamp: Date.now(),
};
set((state) => ({
messages: [...state.messages, userMessage],
isLoading: true,
}));
try {
// 调用 Tauri 后端
const response = await invoke<string>('send_message', {
conversationId: get().currentConversationId,
content,
});
const assistantMessage: Message = {
id: crypto.randomUUID(),
role: 'assistant',
content: response,
timestamp: Date.now(),
};
set((state) => ({
messages: [...state.messages, assistantMessage],
isLoading: false,
}));
} catch (error) {
console.error('发送失败:', error);
set({ isLoading: false });
}
},
clearMessages: () => set({ messages: [] }),
loadConversation: async (id: string) => {
const history = await invoke<Message[]>('get_history', { id });
set({
currentConversationId: id,
messages: history,
});
},
}));
4.2 技能生成向导 UI
这是一个复杂的多步骤表单,使用 State Machine (状态机) 思想管理 UI 状态:
// src/components/wizard/SkillWizardDialog.tsx
type WizardStage =
| 'idle' // 初始状态
| 'parsing' // 解析需求
| 'collecting' // 收集信息(多轮对话)
| 'generating' // 生成 DSL
| 'preview' // 预览确认
| 'saving'; // 保存中
const SkillWizardDialog: React.FC<Props> = ({ open, onOpenChange }) => {
const [stage, setStage] = useState<WizardStage>('idle');
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [generatedSkill, setGeneratedSkill] = useState<Skill | null>(null);
const handleStart = async (description: string) => {
setStage('parsing');
try {
// 开始解析需求
const result = await invoke<WizardResult>('start_skill_wizard', {
description,
});
if (result.needs_more_info) {
setMessages(result.messages);
setStage('collecting');
} else {
setGeneratedSkill(result.skill);
setStage('preview');
}
} catch (error) {
console.error('解析失败:', error);
setStage('idle');
}
};
const handleReply = async (reply: string) => {
const newMessage = { role: 'user', content: reply };
setMessages([...messages, newMessage]);
const result = await invoke<WizardResult>('continue_skill_wizard', {
reply,
});
if (result.completed) {
setGeneratedSkill(result.skill);
setStage('preview');
} else {
setMessages([...messages, newMessage, result.next_question]);
}
};
const handleSave = async () => {
if (!generatedSkill) return;
setStage('saving');
await invoke('save_skill', { skill: generatedSkill });
onOpenChange(false);
};
// 渲染不同阶段的内容
const renderContent = () => {
switch (stage) {
case 'idle':
return (
<div className="space-y-4">
<p className="text-muted-foreground">
描述你想要创建的技能,我会帮你生成配置
</p>
<textarea
placeholder="例如:帮我写一个可以生成周报的技能..."
className="w-full h-32 p-3 border rounded-lg"
onKeyDown={(e) => {
if (e.key === 'Enter' && e.metaKey) {
handleStart(e.currentTarget.value);
}
}}
/>
</div>
);
case 'parsing':
case 'generating':
return (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">
{stage === 'parsing' ? '正在解析需求...' : '正在生成技能...'}
</span>
</div>
);
case 'collecting':
return (
<div className="space-y-4">
{messages.map((msg, i) => (
<div key={i} className={`p-3 rounded-lg ${
msg.role === 'assistant' ? 'bg-muted' : 'bg-primary/10'
}`}>
{msg.content}
</div>
))}
<input
type="text"
placeholder="输入回复..."
className="w-full p-3 border rounded-lg"
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleReply(e.currentTarget.value);
e.currentTarget.value = '';
}
}}
/>
</div>
);
case 'preview':
return (
<div className="space-y-4">
<SkillPreviewCard skill={generatedSkill!} />
<div className="flex gap-2 justify-end">
<Button variant="outline" onClick={() => setStage('idle')}>
重新生成
</Button>
<Button onClick={handleSave}>
保存技能
</Button>
</div>
</div>
);
default:
return null;
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>✨ 创建新技能</DialogTitle>
</DialogHeader>
<div className="py-4">
{renderContent()}
</div>
</DialogContent>
</Dialog>
);
};
五、性能优化与最佳实践
5.1 启动速度优化
-
Lazy Loading (懒加载) :
// 使用 React.lazy 动态加载非首屏路由 const SkillsPage = React.lazy(() => import('./pages/SkillsPage')); const WorkflowPage = React.lazy(() => import('./pages/WorkflowPage')); const SettingsPage = React.lazy(() => import('./pages/SettingsPage')); // 在路由中使用 Suspense 包裹 <Suspense fallback={<LoadingSpinner />}> <Routes> <Route path="/skills" element={<SkillsPage />} /> </Routes> </Suspense> -
数据库预热:
- 在 splash screen 显示期间初始化 SQLite 连接池
- 预加载常用配置数据到内存缓存
-
Rust 编译优化:
# Cargo.toml [profile.release] lto = true # Link Time Optimization codegen-units = 1 # 单代码生成单元,更好优化 strip = true # 去除调试符号
5.2 内存占用优化
-
Rust 侧:
- 避免在内存中缓存大文件内容
- 使用流式处理 (Stream) 替代一次性加载
- 及时释放不再使用的 LLM 上下文
-
Webview 侧:
- 虚拟列表 (Virtual List) 渲染长对话历史
- 定期清理 DOM 中的 Markdown 渲染结果
5.3 安全最佳实践
-
CSP 配置:
// tauri.conf.json { "security": { "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" } } -
命令白名单:
// capabilities/default.json { "permissions": [ "core:default", { "identifier": "shell:allow-open", "allow": [{ "path": "https://*" }] } ] } -
API Key 安全:
- 使用 AES-256-GCM 加密存储
- 密钥从设备指纹派生,不明文保存
- 定期提醒用户更换泄露的 Key
5.4 安全设计细节
5.4.1 API Key 加密存储实现
API Key 作为敏感信息,采用了多层安全保护机制:
// src-tauri/src/services/crypto.rs
use aes_gcm::{Aes256Gcm, Key, Nonce};
use aes_gcm::aead::{Aead, NewAead};
use pbkdf2::pbkdf2;
use hmac::Hmac;
use sha2::Sha256;
/// 加密服务
pub struct CryptoService {
key: Key<Aes256Gcm>,
}
impl CryptoService {
/// 从设备指纹派生密钥
/// 结合主机名、用户名和机器唯一ID生成设备指纹
pub fn from_device_fingerprint() -> Result<Self, AppError> {
// 组合多个设备特征生成唯一指纹
let hostname = whoami::hostname();
let username = whoami::username();
let machine_id = machine_uid::get()
.map_err(|e| AppError::Crypto(format!("获取机器ID失败: {}", e)))?;
let fingerprint = format!("{}-{}-{}", hostname, username, machine_id);
// 使用 PBKDF2 派生 256 位密钥
// 100,000 次迭代,增加暴力破解难度
let salt = b"smart-toolbox-salt-2024";
let mut key = [0u8; 32];
pbkdf2::<Hmac<Sha256>>(
fingerprint.as_bytes(),
salt,
100_000,
&mut key,
);
Ok(Self { key: Key::from_slice(&key).clone() })
}
/// 加密 API Key
pub fn encrypt(&self, plaintext: &str) -> Result<String, AppError> {
let cipher = Aes256Gcm::new(&self.key);
// 注意:实际应用中应使用随机 nonce
let nonce = Nonce::from_slice(b"unique nonce");
let ciphertext = cipher.encrypt(nonce, plaintext.as_bytes())
.map_err(|e| AppError::Crypto(format!("加密失败: {}", e)))?;
// Base64 编码便于存储
Ok(base64::encode(&ciphertext))
}
/// 解密 API Key
pub fn decrypt(&self, ciphertext: &str) -> Result<String, AppError> {
let cipher = Aes256Gcm::new(&self.key);
let nonce = Nonce::from_slice(b"unique nonce");
let ciphertext_bytes = base64::decode(ciphertext)
.map_err(|e| AppError::Crypto(format!("Base64 解码失败: {}", e)))?;
let plaintext = cipher.decrypt(nonce, ciphertext_bytes.as_ref())
.map_err(|e| AppError::Crypto(format!("解密失败: {}", e)))?;
String::from_utf8(plaintext)
.map_err(|e| AppError::Crypto(format!("UTF-8 解码失败: {}", e)))
}
}
安全特性:
- 设备绑定:密钥与设备硬件信息绑定,即使数据库泄露也无法在其他设备上解密
- PBKDF2 加密:100,000 次迭代增加暴力破解难度
- AES-256-GCM:工业级加密算法,提供机密性和完整性保护
5.4.2 权限控制机制
基于 Tauri 2.x 的权限系统实现最小权限原则:
// capabilities/default.json
{
"identifier": "smart-toolbox-core",
"description": "核心功能权限",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open",
"fs:read-all",
"fs:write-all",
{
"identifier": "http:default",
"allow": [
{ "url": "https://api.openai.com/*" },
{ "url": "https://api.anthropic.com/*" },
{ "url": "https://api.deepseek.com/*" }
]
}
]
}
权限控制策略:
- 最小权限原则:只授予应用必需的权限
- URL 白名单:限制 HTTP 请求只能发送到指定的 API 端点
- 窗口隔离:限制功能只能在主窗口中使用
5.4.3 命令白名单机制
所有 Tauri 命令都经过严格验证:
// src-tauri/src/commands/mod.rs
/// 安全的命令调用示例
#[tauri::command]
pub async fn send_message(
app: tauri::AppHandle,
state: tauri::State<'_, AppState>,
conversation_id: Option<String>,
content: String,
) -> Result<String, AppError> {
// 输入验证
if content.trim().is_empty() {
return Err(AppError::InvalidInput("消息内容不能为空".to_string()));
}
// 长度限制
if content.len() > 10000 {
return Err(AppError::InvalidInput("消息内容过长".to_string()));
}
// 权限检查
if !state.is_user_authenticated().await {
return Err(AppError::Unauthorized("用户未认证".to_string()));
}
// 调用核心服务
let result = state.chat_service.send_message(conversation_id, content).await?;
Ok(result)
}
安全检查点:
- 输入验证:检查参数合法性
- 长度限制:防止缓冲区溢出
- 权限验证:确保用户已认证
- 错误处理:避免泄露敏感信息
六、Tauri vs Electron:开发体验深度对比
作为一个有过 Electron 开发经验的开发者,在完成智能工具箱的开发后,我想从多个维度对比一下 Tauri 和 Electron 的开发体验。
6.1 开发环境搭建
| 维度 | Electron | Tauri |
|---|---|---|
| 前置依赖 | Node.js + npm/yarn | Node.js + Rust + Cargo + 系统依赖 |
| 安装复杂度 | ⭐ 简单 | ⭐⭐⭐ 中等(需要配置 Rust 环境) |
| 首次启动 | 1-2 分钟 | 3-5 分钟(首次编译 Rust) |
| 热更新速度 | ⭐⭐⭐⭐⭐ 极快 | ⭐⭐⭐⭐ 快(前端热更新,Rust 需重新编译) |
个人感受:
- Electron 的环境搭建几乎是零门槛,只要会 Node.js 就能上手
- Tauri 需要安装 Rust 工具链,对于没有 Rust 经验的开发者来说有一定学习成本
- 但 Tauri 的
cargo tauri dev命令会自动处理大部分配置,实际体验比预期好很多
6.2 前端开发体验
相似之处:
- 两者都使用 Web 技术栈(HTML/CSS/JavaScript)
- 都可以使用 React、Vue、Angular 等现代前端框架
- 都支持热更新(HMR)
差异之处:
// Electron: 使用 IPC 通信
const { ipcRenderer } = require('electron');
// 发送消息
ipcRenderer.send('message', { data: 'hello' });
// 监听消息
ipcRenderer.on('reply', (event, result) => {
console.log(result);
});
// Tauri: 使用 invoke API
import { invoke } from '@tauri-apps/api/core';
// 调用命令(返回 Promise)
const result = await invoke('get_data', { id: 123 });
// 监听事件
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('event-name', (event) => {
console.log(event.payload);
});
个人感受:
- Tauri 的
invokeAPI 更加现代化,基于 Promise,与 async/await 配合更自然 - Electron 的 IPC 事件模型更灵活,但需要手动管理事件监听器
- Tauri 的类型安全更好(配合 TypeScript),可以在编译时发现错误
6.3 后端开发体验
这是两者差异最大的地方:
Electron (Node.js) :
// main.js
const { ipcMain } = require('electron');
ipcMain.handle('read-file', async (event, filePath) => {
try {
const data = await fs.promises.readFile(filePath, 'utf-8');
return { success: true, data };
} catch (error) {
return { success: false, error: error.message };
}
});
Tauri (Rust) :
// commands.rs
#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
tokio::fs::read_to_string(&path)
.await
.map_err(|e| e.to_string())
}
对比分析:
| 维度 | Electron (Node.js) | Tauri (Rust) |
|---|---|---|
| 类型安全 | ⭐⭐ 弱类型,运行时错误 | ⭐⭐⭐⭐⭐ 强类型,编译时检查 |
| 性能 | ⭐⭐⭐ 单线程,事件循环 | ⭐⭐⭐⭐⭐ 多线程,零成本抽象 |
| 内存安全 | ⭐⭐⭐ 垃圾回收,可能有泄漏 | ⭐⭐⭐⭐⭐ 所有权系统,无泄漏 |
| 学习曲线 | ⭐ 简单,JavaScript 基础即可 | ⭐⭐⭐⭐ 需要理解所有权、生命周期 |
| 开发效率 | ⭐⭐⭐⭐⭐ 快速迭代 | ⭐⭐⭐ 编译时间较长,但 bug 更少 |
| 生态丰富度 | ⭐⭐⭐⭐⭐ npm 生态极其丰富 | ⭐⭐⭐ crates.io 生态在增长 |
个人感受:
- Electron:开发速度快,几乎不需要学习新东西,适合快速原型开发
- Tauri:初期学习成本高,但 Rust 的类型系统让代码更健壮,编译器就像一个严格的代码审查员
- Rust 的所有权模型一开始很难理解,但一旦掌握,会发现它让并发编程变得简单安全
6.4 调试体验
Electron:
- Chrome DevTools 直接调试主进程和渲染进程
console.log随处可用- 断点调试体验与 Web 开发一致
Tauri:
- 前端:Chrome DevTools(与 Electron 相同)
- 后端:需要使用
println!或dbg!宏,或者配置 VS Code 的 Rust 插件进行调试 - 日志查看:终端输出或使用
env_logger等日志库
个人感受:
- Electron 的调试体验更统一,前后端都可以用 DevTools
- Tauri 的 Rust 调试需要额外配置,但 Rust 的错误信息非常详细,编译器会给出很好的提示
- 对于习惯了 Web 开发的我来说,Tauri 的调试体验需要适应
6.5 打包与分发
| 维度 | Electron | Tauri |
|---|---|---|
| 包体积 | 150MB+ | 10-20MB |
| 打包时间 | 2-5 分钟 | 5-10 分钟 |
| 签名配置 | 相对简单 | 需要配置系统签名 |
| 跨平台 | ⭐⭐⭐⭐⭐ 非常成熟 | ⭐⭐⭐⭐ 支持 Windows/macOS/Linux |
个人感受:
- Tauri 的包体积优势非常明显,用户下载体验更好
- Electron 的打包工具(electron-builder)更成熟,配置更简单
- Tauri 的签名配置在不同平台上差异较大,需要查阅文档
6.6 社区与生态
Electron:
- ⭐⭐⭐⭐⭐ 成熟稳定,VS Code、Slack、Discord 等知名应用都在用
- npm 上有大量现成的库和插件
- 问题解决方案丰富,Stack Overflow 上有很多答案
Tauri:
- ⭐⭐⭐ 快速成长中,社区活跃
- 官方文档质量高,但第三方资源相对较少
- 遇到问题时可能需要自己研究或提 issue
个人感受:
- Electron 的生态优势无可替代,几乎任何需求都能找到现成的解决方案
- Tauri 的社区虽然年轻,但非常友好,官方团队响应积极
- 对于 AI 应用这种需要高性能的场景,Tauri 的优势更明显
6.7 总结:什么时候选择哪个?
选择 Electron,如果:
- ✅ 团队熟悉 JavaScript/TypeScript,不想学习新语言
- ✅ 需要快速开发原型,时间紧迫
- ✅ 应用需要大量 Node.js 生态的库
- ✅ 包体积和内存占用不是主要考虑因素
- ✅ 需要非常成熟的跨平台支持
选择 Tauri,如果:
- ✅ 追求极致的性能和用户体验
- ✅ 包体积和内存占用是关键指标
- ✅ 愿意学习 Rust,享受类型安全带来的好处
- ✅ 应用需要处理大量计算或 I/O 操作
- ✅ 对安全性有较高要求
- ✅ 想要尝试新技术,拓展技术边界
我的选择:
对于智能工具箱这个项目,我选择了 Tauri,主要原因是:
- 性能需求:AI 应用需要频繁调用 LLM API,Rust 的异步性能优势明显
- 用户体验:15MB 的包体积让用户更容易接受
- 学习动机:我想借此机会学习 Rust,拓展技术栈
- 安全性:API Key 等敏感信息的加密存储,Rust 的内存安全特性更让人放心
虽然开发过程中遇到了不少挑战,但整体来说,这是一次非常值得的技术探索。Rust 的学习曲线虽然陡峭,但掌握后的成就感也是无与伦比的。
七、踩坑记录
7.1 Tauri 2.x 升级问题
从 Tauri 1.x 升级到 2.x 时遇到的主要问题:
| 问题 | 原因 | 解决方案 |
|---|---|---|
invoke 参数序列化失败 | 2.x 对类型检查更严格 | 确保 Rust 结构体派生 Serialize |
| 命令无权限执行 | 新版权限系统 | 配置 capabilities/default.json |
| 插件 API 变化 | 2.x 重构了插件接口 | 升级到对应 2.x 版本的插件 |
7.2 异步 Rust 与 Tauri
// ❌ 错误:在 async 上下文中使用同步锁
async fn bad_example(state: State<'_, Mutex<AppState>>) {
let guard = state.lock().unwrap(); // 可能死锁!
some_async_operation().await; // 持有锁时 await
}
// ✅ 正确:使用 tokio::sync::Mutex
use tokio::sync::Mutex;
async fn good_example(state: State<'_, Mutex<AppState>>) {
let guard = state.lock().await;
some_async_operation().await;
}
7.3 流式响应处理
// 使用 Tauri event 实现流式推送
use tauri::Emitter;
#[tauri::command]
async fn stream_chat(
app: tauri::AppHandle,
message: String,
) -> Result<(), AppError> {
let mut stream = llm_client.chat_stream(message).await?;
while let Some(chunk) = stream.next().await {
// 通过事件推送给前端
app.emit("chat-chunk", &chunk)?;
}
app.emit("chat-done", ())?;
Ok(())
}
// 前端监听事件
import { listen } from '@tauri-apps/api/event';
useEffect(() => {
const unlisten = listen<string>('chat-chunk', (event) => {
setContent((prev) => prev + event.payload);
});
return () => { unlisten.then(fn => fn()); };
}, []);
八、总结与展望
通过 Tauri 2.x + Rust + React 的组合,我们成功构建了一个轻量级、高性能、功能强大的桌面 AI 助手。
技术收益
| 维度 | 成果 | 具体数据 |
|---|---|---|
| 包体积 | 15MB,是同等功能 Electron 应用的 1/10 | Tauri: 15MB, Electron: 150MB+ |
| 内存占用 | 运行时约 80MB,远低于 Electron 的 300MB+ | Tauri: 80MB, Electron: 300MB+ |
| 启动时间 | 秒级启动 | 冷启动: 1.2s, 热启动: 0.3s |
| 安全性 | Rust 内存安全 + Tauri 最小权限 | 0 安全漏洞报告 |
| 开发效率 | React 生态 + Rust 强类型,bug 更少 | 编译时捕获 90% 以上错误 |
V2.0 已完成功能
- ✅ 多模型 LLM 接入(OpenAI/Claude/DeepSeek/360AI/Ollama)
- ✅ 对话式技能生成向导
- ✅ 技能管理与执行(Prompt/Script/Workflow)
- ✅ Token 优化(Prompt 分级、会话复用)
- ✅ 工作流可视化编排
- ✅ 平台发布(掘金)
- ✅ Agent Skills 兼容层(双向转换、生态互通)
V3.0 规划
3.1 技能市场
功能描述:
基于 GitHub 的去中心化技能市场,用户可以分享、发现、安装社区技能。
技术方案:
- 数据源:GitHub Issues + GitHub Releases
- 技能索引:使用 GitHub API 搜索带有特定标签(如
smart-toolbox-skill)的仓库 - 安装机制:克隆仓库到本地技能目录,解析 SKILL.md
- 版本管理:基于 Git tag 进行版本控制
技术挑战:
- GitHub API 速率限制(60 次/小时未认证,5000 次/小时已认证)
- 技能安全性验证(防止恶意代码)
- 网络环境问题(国内访问 GitHub 不稳定)
缓解策略:
- 实现本地缓存机制,减少 API 调用
- 技能沙箱执行,限制文件系统访问权限
- 提供 Gitee 镜像源作为备选
3.2 连接器系统
功能描述:
统一的平台接入标准,支持快速扩展新的发布平台。
技术方案:
// 连接器接口定义
pub trait PlatformConnector {
async fn publish(&self, content: &PublishContent) -> Result<PublishResult>;
async fn validate_config(&self, config: &Value) -> Result<()>;
fn get_config_schema(&self) -> ConfigSchema;
}
// 连接器注册表
pub struct ConnectorRegistry {
connectors: HashMap<String, Box<dyn PlatformConnector>>,
}
技术挑战:
- 不同平台的认证机制差异(OAuth、API Key、Cookie)
- 内容格式转换(Markdown → 各平台富文本)
- 反爬虫机制应对
缓解策略:
- 提供统一的认证抽象层
- 使用 HTML 解析库(如 scraper)处理富文本
- 实现请求限流和代理池
3.3 更多平台支持
计划平台:
- 微信公众号(需要 Cookie 认证)
- 知乎专栏(支持 Markdown)
- CSDN(API 接口)
- 简书(Markdown 支持)
技术分析:
- 微信公众号:难度 ⭐⭐⭐⭐⭐,需要模拟登录,反爬严格
- 知乎:难度 ⭐⭐⭐,有官方 API,但需要处理验证码
- CSDN:难度 ⭐⭐,API 相对开放
- 简书:难度 ⭐⭐,直接支持 Markdown
3.4 工作流增强
新增功能:
- 循环节点:支持
for循环、while循环 - 定时触发:Cron 表达式支持
- 条件分支:更复杂的条件判断
- 并行执行:多个节点并行运行
技术方案:
// 循环节点示例
pub struct LoopNode {
pub loop_type: LoopType, // For / While
pub iterations: Option<u32>,
pub condition: Option<String>,
pub body: Vec<WorkflowNode>,
}
// 定时触发器
pub struct CronTrigger {
pub expression: String, // "0 0 * * *" 每天零点
pub timezone: String,
}
技术挑战:
- 循环可能导致无限执行,需要设置超时
- 并行执行的状态同步
- Cron 表达式解析和调度
缓解策略:
- 设置最大循环次数和超时时间
- 使用 Rust 的
tokio::spawn实现并行 - 集成
cron库处理定时任务
3.5 风险评估与优先级
| 功能 | 风险等级 | 优先级 | 预计工期 | 依赖关系 |
|---|---|---|---|---|
| 连接器系统 | 🟡 中 | P0 | 3-4 周 | 无 |
| 工作流增强 | 🟡 中 | P0 | 3-4 周 | 无 |
| 技能市场 | 🔴 高 | P1 | 4-6 周 | 连接器系统 |
| 公众号支持 | 🔴 高 | P2 | 2-3 周 | 连接器系统 |
| 知乎支持 | 🟢 低 | P2 | 2-3 周 | 连接器系统 |
如果你对 Tauri 桌面开发或 AI Agent 开发感兴趣,欢迎在评论区交流!
相关资源:
- Tauri 官方文档:tauri.app
- Rust 异步编程:rust-lang.github.io/async-book/
- shadcn/ui 组件库:ui.shadcn.com
- Agent Skills 规范:docs.anthropic.com/en/docs/bui…