LJ.

实战项目:Tauri + AI SDK 打造桌面 AI Agent

前端开发·

场景:你需要一个本地文件助手

假设这个需求:

我有一个文件夹,里面有几百个 Markdown 文档。我想问 AI:"帮我找出所有关于 TypeScript 的笔记",或者"总结一下上周的工作日志"。

Web 方案的问题

  • 上传文件到服务器?隐私风险。
  • 浏览器读文件?权限受限。

桌面 App 的优势

  • 直接访问本地文件系统
  • 无需上传,数据不出本地
  • 可以调用系统 API

这就是桌面 AI Agent 的典型场景。今天我们用 Tauri + Vercel AI SDK 来实现它。


项目目标

功能需求

  1. 选择本地文件夹
  2. 读取文件夹内的文本文件(.md / .txt)
  3. 将内容发送给 AI
  4. AI 根据用户问题回答

技术栈

  • 前端:React + TypeScript
  • 桌面框架:Tauri
  • AI 集成:Vercel AI SDK
  • LLM:Claude Sonnet 4

架构设计

用户界面(React)
    ↓
Tauri IPC
    ↓
Rust 后端
    ├─ 读取文件系统
    └─ 调用 AI API
         ↓
    Claude API
         ↓
    流式响应返回前端

为什么这样设计?

  1. Tauri IPC: 前端无法直接访问文件系统,通过 Rust 后端桥接
  2. Rust 后端: 安全、高效处理文件 I/O
  3. Vercel AI SDK: 简化 AI 调用,自动处理流式响应

Step 1: 创建 Tauri 项目

1.1 初始化项目

# 安装 Tauri CLI
npm install -g @tauri-apps/cli
 
# 创建项目
npm create tauri-app@latest
 
# 选择:
# - Template: React + TypeScript
# - Name: file-agent

1.2 项目结构

file-agent/
├── src/              # React 前端
│   ├── App.tsx
│   └── main.tsx
├── src-tauri/        # Rust 后端
│   ├── src/
│   │   └── main.rs   # 核心逻辑
│   └── Cargo.toml
└── package.json

Step 2: 实现文件读取(Rust 后端)

2.1 添加依赖

编辑 src-tauri/Cargo.toml

[dependencies]
tauri = { version = '1.5', features = ['dialog-open', 'fs-read-dir'] }
serde = { version = '1.0', features = ['derive'] }
serde_json = '1.0'

2.2 创建 Tauri Command

编辑 src-tauri/src/main.rs

use std::fs;
use std::path::Path;
use tauri::command;
 
#[command]
fn read_folder(folder_path: String) -> Result<Vec<FileContent>, String> {
    let mut files = Vec::new();
 
    // 读取文件夹
    let entries = fs::read_dir(&folder_path)
        .map_err(|e| e.to_string())?;
 
    for entry in entries {
        let entry = entry.map_err(|e| e.to_string())?;
        let path = entry.path();
 
        // 只处理 .md 和 .txt 文件
        if let Some(ext) = path.extension() {
            if ext == 'md' || ext == 'txt' {
                let content = fs::read_to_string(&path)
                    .map_err(|e| e.to_string())?;
 
                files.push(FileContent {
                    path: path.to_str().unwrap().to_string(),
                    name: path.file_name().unwrap().to_str().unwrap().to_string(),
                    content,
                });
            }
        }
    }
 
    Ok(files)
}
 
#[derive(serde::Serialize)]
struct FileContent {
    path: String,
    name: String,
    content: String,
}
 
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![read_folder])
        .run(tauri::generate_context!())
        .expect('error while running tauri application');
}

Step 3: 集成 AI SDK(前端)

3.1 安装 Vercel AI SDK

npm install ai
npm install @anthropic-ai/sdk

3.2 创建 AI 助手组件

创建 src/FileAgent.tsx

import { useState } from 'react';
import { useChat } from 'ai/react';
import { invoke } from '@tauri-apps/api/tauri';
 
interface FileContent {
  path: string;
  name: string;
  content: string;
}
 
export function FileAgent() {
  const [files, setFiles] = useState<FileContent[]>([]);
  const [folderPath, setFolderPath] = useState('');
 
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    api: '/api/chat', // 本地 API 路由
    body: {
      files: files.map(f => ({
        name: f.name,
        content: f.content.slice(0, 1000) // 限制长度
      }))
    }
  });
 
  // 选择文件夹
  const selectFolder = async () => {
    const selected = await open({
      directory: true,
      multiple: false,
    });
 
    if (selected) {
      setFolderPath(selected as string);
 
      // 调用 Rust 后端读取文件
      const fileList = await invoke<FileContent[]>('read_folder', {
        folderPath: selected
      });
 
      setFiles(fileList);
    }
  };
 
  return (
    <div className='container'>
      <h1>本地文件助手</h1>
 
      {/* 文件夹选择 */}
      <button onClick={selectFolder}>
        选择文件夹
      </button>
      {folderPath && <p>已选择: {folderPath}</p>}
 
      {/* 文件列表 */}
      {files.length > 0 && (
        <div>
          <h3>找到 {files.length} 个文件</h3>
          <ul>
            {files.map(f => (
              <li key={f.path}>{f.name}</li>
            ))}
          </ul>
        </div>
      )}
 
      {/* 聊天界面 */}
      <div className='chat'>
        {messages.map(m => (
          <div key={m.id} className={`message ${m.role}`}>
            <strong>{m.role}:</strong> {m.content}
          </div>
        ))}
      </div>
 
      {/* 输入框 */}
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={handleInputChange}
          placeholder='问我关于这些文件的任何问题...'
          disabled={files.length === 0}
        />
        <button type='submit'>发送</button>
      </form>
    </div>
  );
}

3.3 创建 API 路由

由于 Tauri 是桌面应用,我们需要一个简单的本地 HTTP 服务。

创建 src/api/chat.ts

import Anthropic from "@anthropic-ai/sdk"
 
const anthropic = new Anthropic({
  apiKey: import.meta.env.VITE_ANTHROPIC_API_KEY,
})
 
export async function POST(req: Request) {
  const { messages, files } = await req.json()
 
  // 构建上下文
  const fileContext = files
    .map((f: any) => `文件: ${f.name}\n${f.content}`)
    .join("\n\n---\n\n")
 
  const systemPrompt = `你是一个本地文件助手。用户有以下文件:\n\n${fileContext}\n\n请根据这些文件回答用户的问题。`
 
  // 调用 Claude
  const stream = await anthropic.messages.create({
    model: "claude-sonnet-4",
    max_tokens: 1024,
    messages: [{ role: "system", content: systemPrompt }, ...messages],
    stream: true,
  })
 
  // 返回流式响应
  return new Response(
    new ReadableStream({
      async start(controller) {
        for await (const chunk of stream) {
          if (chunk.type === "content_block_delta") {
            controller.enqueue(chunk.delta.text)
          }
        }
        controller.close()
      },
    }),
  )
}

Step 4: 运行和测试

4.1 设置 API Key

创建 .env

VITE_ANTHROPIC_API_KEY=sk-ant-xxx

4.2 启动开发

# 启动 Tauri 开发模式
npm run tauri dev

4.3 测试流程

  1. 选择文件夹 → 点击"选择文件夹"
  2. 查看文件列表 → 确认读取成功
  3. 提问 → "总结一下这些文件的主题"
  4. 查看回答 → AI 基于文件内容回复

Step 5: 打包发布

5.1 构建应用

npm run tauri build

5.2 输出位置

src-tauri/target/release/bundle/
├── dmg/           # macOS
├── msi/           # Windows
└── deb/           # Linux

5.3 分发

  • macOS: 双击 .dmg 安装
  • Windows: 运行 .msi 安装
  • Linux: sudo dpkg -i xxx.deb

核心技术点解析

1. Tauri IPC 通信

// 前端调用 Rust
const result = await invoke<T>('command_name', { param: value });
 
// Rust 定义 Command
#[command]
fn command_name(param: String) -> Result<T, String> {
    // ...
}

关键:前后端通过 invoke 通信,类型安全。


2. 文件系统访问

Web 限制

  • 浏览器只能访问用户主动选择的文件
  • 无法遍历文件夹

Tauri 解决

  • Rust 后端直接调用系统 API
  • fs::read_dir 遍历文件夹
  • 权限控制在 tauri.conf.json

3. AI 流式响应

const { messages, handleSubmit } = useChat({
  api: "/api/chat",
  onFinish: (message) => {
    console.log("AI 回复完成", message)
  },
})

Vercel AI SDK 自动处理:

  • 流式数据接收
  • UI 实时更新
  • 错误处理

扩展方向

1. 增强文件处理

  • 支持 PDF、Word
  • 图片 OCR
  • 代码语法高亮

2. 本地向量数据库

npm install chromadb

存储文件向量,实现语义搜索。

3. 多模型支持

const model = userChoice === "claude" ? "claude-sonnet-4" : "gpt-4"

4. 历史记录

// 存储对话历史到 SQLite
use rusqlite::Connection;
 
let conn = Connection::open('agent.db')?;
conn.execute(
    'INSERT INTO chats (question, answer) VALUES (?1, ?2)',
    params![question, answer],
)?;

常见问题

Q1: 如何处理大文件?

A: 限制单文件大小,或分块读取:

const MAX_SIZE: usize = 1024 * 1024; // 1MB
 
if content.len() > MAX_SIZE {
    return Err('文件过大'.to_string());
}

Q2: 如何保护 API Key?

A: 使用环境变量 + 打包时注入:

// vite.config.ts
export default {
  define: {
    __API_KEY__: JSON.stringify(process.env.ANTHROPIC_API_KEY),
  },
}

Q3: 能否支持 Windows/Linux?

A: Tauri 跨平台,一次开发,三端运行。只需:

npm run tauri build -- --target universal-apple-darwin  # macOS
npm run tauri build -- --target x86_64-pc-windows-msvc  # Windows
npm run tauri build -- --target x86_64-unknown-linux-gnu # Linux

行动清单

如果你要开始这个项目:

  1. 今天 - 搭建 Tauri 基础项目
  2. 明天 - 实现文件读取功能
  3. 后天 - 集成 AI SDK
  4. 第4天 - 完善 UI,打包测试

GitHub 模板file-agent-template(待创建)


总结

这个项目展示了:

  • ✅ Tauri 的文件系统访问能力
  • ✅ Rust 与 TypeScript 的协作
  • ✅ Vercel AI SDK 的简洁集成
  • ✅ 桌面 AI Agent 的完整流程

关键收获

  • 桌面应用 = Web 技能 + 系统能力
  • AI SDK 让集成变得简单
  • Tauri 性能 > Electron,包体积 < 10MB

这是「AI 时代前端转型」系列的第 6 篇。下一篇我们聊聊转型期的生存策略。