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:stream、cli:status、cli: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:anthropic、openai、google。

设计理念

微信风格界面

参考微信的设计语言:

  • 配色:以绿色(#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 对 dangerouslySetInnerHTML 和 children 共存会直接崩溃:

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...


参考资源:

相关推荐
sell-cloud2 小时前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
Navicat中国3 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI4 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区4 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
XCDoris6 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy6 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V6 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI6 小时前
AI英语教培智能体的开发
人工智能
物联网软硬件开发-轨物科技6 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人
海宇数据6 小时前
零信任架构实战:基于海宇网上踪迹欺诈行为识别构建自动化信贷资信评估网关
运维·人工智能·架构·自动化