talkcozy像聊微信一样多项目同时开发

用 Tauri 2.x + React 19 打造微信风格的 AI 桌面客户端

一个 533 个 Rust crate、77 个前端测试、完整 SQLite 持久化的桌面应用是如何诞生的?

前言

周一早上,老板路过小明工位,看到一个绿色的聊天界面。

"上班时间聊微信?!"老板的脸瞬间黑了。

小明淡定地转过屏幕:"老板您看,这是 TalkCozy,我在让 AI 帮我重构代码呢。"

老板凑近一看:嚯,这对话框里全是代码,还有语法高亮、Diff 对比、Mermaid 流程图...

"这玩意儿...能帮我写周报吗?"

"能。"

"能帮我Review代码吗?"

"能。"

"能帮我把这个月加班费算一下吗?"

"老板,这个 AI 不能替代财务,但它能帮你生成一个加班费计算器。"

老板若有所思地点点头,转身就走。走到门口又回头问了一句:"这个软件叫什么来着?"

"TalkCozy,用 Tauri 开发的,React 19 + Rust。"

"好,记住了。那个...能给我也装一个吗?"


作为一个 macOS 用户,我一直想要一个原生体验的 AI 助手客户端。Web 版的 ChatGPT 和 Claude 虽然功能强大,但总觉得少了点什么------可能是 Command+Q 就能退出的爽快感,可能是 Dock 栏那个熟悉的小图标,也可能是系统原生通知带来的归属感。

更重要的是,我每天都在用 Claude Code CLI 进行开发工作。它很强大,但终端界面毕竟有局限性:会话管理靠文件系统,历史记录靠滚动翻页,多项目切换更是噩梦。于是我萌生了一个想法:能不能给 Claude Code 包一层微信风格的 GUI?

经过几个月的开发,TalkCozy 诞生了。本文将分享这个项目的技术架构和开发过程中的经验。

项目概览

TalkCozy 是一个 macOS 原生 AI 助手客户端,核心特性包括:

  • 会话管理:多项目会话,支持置顶、归档、工作流状态标记(todo/in_progress/done)
  • 实时流式对话:Shiki 语法高亮、Mermaid 图表渲染、Unified Diff 展示
  • 多 Provider 支持:Anthropic / OpenAI / Google AI,API Key 安全存储于 macOS Keychain
  • 文件树浏览:实时变更检测,文件内容预览
  • Agent 任务面板:工具调用追踪,消息关联历史浏览
  • Slash 命令/clear/model/help

技术栈一览

技术 版本
桌面框架 Tauri (Rust) 2.x
前端 React + TypeScript 19.2 + 5.8
构建 Vite 7.x
样式 Tailwind CSS 4.x
状态管理 Zustand 5.x
数据库 SQLite (rusqlite) -
测试 Vitest + Playwright -

架构设计

整体架构

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                    Frontend (React 19)                       │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │
│  │  Chat    │ │  File    │ │  Agent   │ │ Settings │       │
│  │  View    │ │  Tree    │ │  Panel   │ │  Panel   │       │
│  └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘       │
│       │            │            │            │              │
│       └────────────┴────────────┴────────────┘              │
│                          │                                   │
│                   Zustand Stores                            │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │
│  │ session  │ │   chat   │ │  agent   │ │   ui     │       │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │
└──────────────────────────┬──────────────────────────────────┘
                           │ Tauri IPC
┌──────────────────────────┴──────────────────────────────────┐
│                    Backend (Rust)                            │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │
│  │ Sidecar  │ │    DB    │ │    FS    │ │Security  │       │
│  │ Manager  │ │ (SQLite) │ │ Watcher  │ │Keychain  │       │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │
└─────────────────────────────────────────────────────────────┘
                           │
                    Claude CLI

前后端通信

Tauri 采用 IPC(进程间通信)机制连接前端和后端:

  • Commands (前端 → Rust):类型安全的 invoke 调用,定义在 src/lib/ipc.ts
  • Events (Rust → 前端):实时流式事件,如 cli:streamcli:statuscli:error

以发送消息为例:

bash 复制代码
用户发送消息 → sendChat IPC → Rust send_chat command
              ↓
Rust 写入 sidecar stdin → Sidecar 处理 → 输出到 stdout
              ↓
handler.rs 解析 → OutputBuffer 批处理(16ms)
              ↓
cli:stream 事件 → 前端 useStreamChat hook
              ↓
StreamingBubble 渲染 + 打字机效果
              ↓
done 事件 → 保存到 SQLite → 关联 Agent 任务

核心功能实现

1. Sidecar 进程管理

这是整个项目最复杂的部分。我们需要管理 Claude CLI 子进程的生命周期:

rust 复制代码
// src-tauri/src/sidecar/manager.rs
pub struct SidecarManager {
    process: Option<Child>,
    stdin: Option<ChildStdin>,
    output_buffer: OutputBufferManager,
    dispatch_queue: DispatchQueue,
    // ...
}

impl SidecarManager {
    pub fn spawn(&mut self, session_id: &str) -> Result<()> {
        // 使用 PTY 包装解决缓冲问题
        let mut cmd = Command::new("script");
        cmd.args(["-q", "/dev/null"])
           .arg(&self.sidecar_path)
           .arg("--resume")
           .arg("--permission-mode")
           .arg("bypassPermissions")
           .envs(self.env_vars());
        
        let child = cmd.spawn()?;
        // ...
    }
}

关键技术点:

  1. PTY 包装 :使用 script -q /dev/null 创建伪终端,解决 CLI 输出缓冲问题
  2. 输出批处理:16ms 间隔(约 60fps)批量发送事件,避免前端渲染抖动
  3. 命令队列:防止命令交错,每个 session 独立队列
  4. 看门狗:120s 无响应检测,自动发送 synthetic done 事件

2. 流式对话渲染

前端使用自定义 hook 处理流式数据:

typescript 复制代码
// src/hooks/useStreamChat.ts
export function useStreamChat(sessionId: string) {
  const addMessage = useChatStore((s) => s.addMessage);
  const setStreamingContent = useChatStore((s) => s.setStreamingContent);
  
  useEffect(() => {
    const unlisten = listen<StreamEvent>('cli:stream', (event) => {
      const { type, content, seq } = event.payload;
      
      if (type === 'token') {
        // 使用 requestAnimationFrame 累积 tokens
        rafAccumulator.add(content, seq);
      } else if (type === 'done') {
        // 保存消息到数据库
        addMessage(sessionId, assistantMessage);
        setStreamingContent(null);
      }
    });
    
    return () => { unlisten.then(f => f()); };
  }, [sessionId]);
}

打字机效果 :由于 CLI -p 模式并非真正的流式输出(而是一次性返回完整内容),我们实现了前端的打字机效果:

typescript 复制代码
// src/hooks/useTypewriter.ts
export function useTypewriter(content: string | null) {
  const [displayed, setDisplayed] = useState('');
  const rafRef = useRef<number>();
  
  useEffect(() => {
    if (!content) return;
    
    const animate = () => {
      setDisplayed(prev => {
        if (prev.length >= content.length) return prev;
        // 自适应速度:根据剩余内容量调整
        const speed = calculateSpeed(content.length - prev.length);
        return content.slice(0, prev.length + speed);
      });
      rafRef.current = requestAnimationFrame(animate);
    };
    
    rafRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(rafRef.current!);
  }, [content]);
  
  return displayed;
}

3. 状态管理(React 19 兼容)

React 19 的 useSyncExternalStore 对状态管理提出了严格要求。Zustand selector 必须返回稳定引用:

typescript 复制代码
// ❌ 错误:每次渲染创建新对象 → 无限循环
const { setNodes, setRootPath } = useFileTreeStore();

// ✅ 正确:逐个订阅
const setNodes = useFileTreeStore((s) => s.setNodes);
const setRootPath = useFileTreeStore((s) => s.setRootPath);

// ❌ 错误:selector 内创建新数组
const ports = useStore((s) => s.data?.ports) ?? [];

// ✅ 正确:模块级常量兜底
const EMPTY: number[] = [];
const ports = useStore((s) => s.data?.ports ?? EMPTY);

4. 数据库设计

使用 SQLite 存储会话和消息,支持全文搜索:

sql 复制代码
-- 会话表
CREATE TABLE sessions (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  project_path TEXT,
  workflow_status TEXT DEFAULT 'active',
  is_archived INTEGER DEFAULT 0,
  pinned_at INTEGER,
  created_at INTEGER,
  updated_at INTEGER
);

-- 消息表 + FTS5
CREATE TABLE messages (
  id TEXT PRIMARY KEY,
  session_id TEXT NOT NULL,
  role TEXT NOT NULL,
  content TEXT NOT NULL,
  created_at INTEGER,
  FOREIGN KEY (session_id) REFERENCES sessions(id)
);

CREATE VIRTUAL TABLE messages_fts USING fts5(
  content, 
  content='messages', 
  content_rowid='rowid'
);

5. 安全的 API Key 存储

使用 macOS Keychain 存储敏感信息:

rust 复制代码
// src-tauri/src/security/keychain.rs
use keyring::Entry;

pub fn store_api_key(provider: &str, key: &str) -> Result<()> {
    let entry = Entry::new("talkcozy", provider)?;
    entry.set_password(key)?;
    Ok(())
}

pub fn get_api_key(provider: &str) -> Result<Option<String>> {
    let entry = Entry::new("talkcozy", provider)?;
    match entry.get_password() {
        Ok(key) => Ok(Some(key)),
        Err(keyring::Error::NoEntry) => Ok(None),
        Err(e) => Err(e.into()),
    }
}

支持多 Provider:anthropicopenaigoogle

设计理念

微信风格界面

参考微信的设计语言:

  • 配色:以绿色(#07C160)为主色调,搭配浅灰背景
  • 气泡:用户消息为微信绿,AI 消息为白色
  • 无硬边框:使用阴影而非边框营造层次感
  • 方形头像:4px 圆角,简洁大方

Apple 设计系统

项目还参考了 Apple 的设计原则:

  • 字体:SF Pro Display(大标题)+ SF Pro Text(正文)
  • 留白:大量留白,让内容呼吸
  • 单一强调色:蓝色仅用于交互元素
  • 无阴影卡片:通过背景色对比而非阴影区分层级
css 复制代码
/* 深色模式 */
:root {
  --background: #000000;
  --foreground: #ffffff;
  --primary: #07C160;  /* 微信绿 */
  --muted: #272729;
  --border: #3a3a3c;
}

/* 浅色模式 */
.light {
  --background: #f5f5f7;
  --foreground: #1d1d1f;
  --primary: #07C160;
  --muted: #f0f0f0;
  --border: #d9d9d9;
}

测试策略

三层测试体系

bash 复制代码
# 第一层:编译 + lint(秒级)
pnpm check          # tsc + eslint + cargo check

# 第二层:单元测试
pnpm test           # Rust test + Vitest

# 第三层:E2E 测试
pnpm test:e2e       # Playwright
pnpm test:tauri-e2e # WebdriverIO(真实 Tauri)

测试覆盖

  • Rust 测试:协议解析、端口检测、DB CRUD/FTS、文件树扫描、Sidecar 管理
  • 前端单元测试:所有 Zustand stores、组件、工具函数
  • E2E 测试:应用启动、侧边栏交互、键盘快捷键、设置面板

开发中的坑

1. Cargo 缓存损坏

如果 ~/.cargo/registry 是符号链接指向 RAMDisk,可能会遇到目录创建失败的问题。解决方案:

bash 复制代码
# 确保 RAMDisk 已挂载并创建必要目录
mkdir -p /Volumes/RAMCache/cargo-registry/{cache,index}

2. PTY 控制字符

Sidecar 通过 PTY 运行时会混入控制字符,需要在解析 JSON 前过滤:

rust 复制代码
// src-tauri/src/sidecar/protocol.rs
pub fn parse_line(line: &str) -> Result<ProtocolMessage> {
    // 跳过 PTY 控制字符,找到第一个 JSON 对象
    let start = line.find('{').ok_or_else(|| anyhow!("No JSON found"))?;
    let json = &line[start..];
    let msg: ProtocolMessage = serde_json::from_str(json)?;
    Ok(msg)
}

3. React 19 严格模式

React 19 对 dangerouslySetInnerHTMLchildren 共存会直接崩溃:

tsx 复制代码
// ❌ 崩溃!
<div dangerouslySetInnerHTML={{ __html: html }}>
  {!html && <span>fallback</span>}
</div>

// ✅ 正确:条件渲染
{html ? (
  <div dangerouslySetInnerHTML={{ __html: html }} />
) : (
  <div><span>fallback</span></div>
)}

总结

TalkCozy 是一个典型的"小而美"项目:

  • 代码量:前端约 15000 行,Rust 约 8000 行
  • 依赖:前端 49 个依赖,Rust 533 个 crate
  • 测试:77 个前端测试 + 50 个 Rust 测试
  • 构建产物:约 15MB(未压缩)

如果你也在开发类似的桌面应用,希望这篇文章能给你一些启发。项目已开源,欢迎 Star 和 PR! github.com/talkcozy/ta...


参考资源

相关推荐
deephub1 小时前
LangChain 还是 LangGraph?一个是编排一个是工具包
人工智能·langchain·大语言模型·langgraph
OidEncoder2 小时前
编码器分辨率与机械精度的关系
人工智能·算法·机器人·自动化
Championship.23.242 小时前
Harness工程深度解析:从理论到实践的完整指南
人工智能·harness
扬帆破浪2 小时前
开源免费的WPS AI 软件 察元AI文档助手:链路 002:executeAssistantFromRibbon 与任务进度窗
人工智能·开源·wps
叶子Talk3 小时前
GPT-Image-2正式发布:文字渲染99%,Image Arena三项第一,AI图像生成彻底变天了
人工智能·gpt·计算机视觉·ai·openai·图像生成·gpt-image-2
不知名的老吴3 小时前
逆转训练针对大语言模型逆转训练的重要性
人工智能·深度学习·语言模型
pingao1413783 小时前
智联未来:4G温湿度传感器如何重塑数据监测新生.态
大数据·网络·人工智能
程序媛小鱼3 小时前
《All in RAG》学习笔记
人工智能
weixin_446260853 小时前
2026年IT技术趋势预测:从AIGC的狂热到Agent生态的底层重塑
人工智能·aigc