Files
EzVibe/docs/update/rust-refactor-design.md
e2hang 454f88d2ab docs: add Rust重构设计说明 - Tauri + Rust单进程架构
- 新增 docs/update/rust-refactor-design.md (1960行/66KB)
- 涵盖11个章节:整体架构/模块设计/通信协议/进程模型/库选型等
- 设计目标:轻量化/极高能效/透明窗口/Live2D集成
- 基于原Python代码完整调研后的架构设计
2026-05-27 14:58:22 +08:00

65 KiB
Raw Permalink Blame History

EzVibe Rust 重构设计说明文档

文档版本: v1.0
编写日期: 2026-05-27
状态: 初稿


1. 设计目标与约束

1.1 核心设计目标

EzVibe 的 Rust 重构围绕以下四个核心目标展开:

目标 描述 量化指标
轻量化 保持宠物桌面的轻量特性,不引入沉重的运行时依赖 二进制体积 < 15MB含 Live2D 资源)
效率极高 极低的 CPU 占用,极快的响应时间 静息 CPU < 1%TTFT < 50ms
功耗极低 适合后台常驻,不影响系统续航 内存占用 < 80MB
能效比极强 用最少的资源完成最多的工作 每小时交互成本最优

1.2 技术约束

  1. 透明窗口: 宠物需要显示在桌面上方,需要 transparent + 无边框窗口
  2. Live2D 模型渲染: 前端需要原生支持 Canvas/WebGL用于渲染 Live2D Cubism 模型
  3. 跨平台: 至少支持 Windows/macOS/Linux 三大平台
  4. LLM 后端兼容: 必须支持 Ollama本地和 OpenAI云端两套 LLM 接口
  5. 数据持久化: 记忆系统需要可靠的状态存储

1.3 非功能约束

  • 启动时间: 应用从点击到可见 < 2 秒
  • 内存回收: 长时间运行不出现内存泄漏
  • 错误恢复: 部分模块失败不影响整体运行
  • 配置友好: 所有参数可通过配置文件或环境变量调整

2. 整体架构

2.1 架构概览

┌─────────────────────────────────────────────────────────────────────────────┐
│                              Tauri Frontend (React)                        │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐   │
│  │ Live2D      │  │ Window      │  │ WebSocket   │  │ State           │   │
│  │ Canvas      │  │ Manager     │  │ Client      │  │ Management      │   │
│  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────────┘   │
│                              ↕ Tauri IPC (invoke + events)                │
├─────────────────────────────────────────────────────────────────────────────┤
│                              Rust Backend (Single Process)                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐   │
│  │ emotion     │  │ memory      │  │ brain       │  │ scheduler       │   │
│  │ Engine      │  │ System      │  │ (LLM)       │  │ (Tasks)         │   │
│  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────────┘   │
│                            ↕                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐                        │
│  │ api         │  │ main        │  │ config      │                        │
│  │ (Tauri Cmds)│  │ Entry       │  │ Manager     │                        │
│  └─────────────┘  └─────────────┘  └─────────────┘                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                              External Services                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐                        │
│  │ Ollama      │  │ OpenAI      │  │ SQLite      │                        │
│  │ (Local)     │  │ (Cloud)     │  │ (Local DB)  │                        │
│  └─────────────┘  └─────────────┘  └─────────────┘                        │
└─────────────────────────────────────────────────────────────────────────────┘

2.2 模块依赖关系图

emotion.rs (无依赖)
       │
       ├──→ brain.rs (依赖 emotion, memory)
       │           │
       │           ├──→ memory.rs (无依赖)
       │           │
       │           └──→ LLM Providers (Ollama/OpenAI)
       │
       ├──→ scheduler.rs (依赖 emotion)
       │           │
       │           └──→ brain.rs (用于主动行为触发)
       │
       ├──→ api.rs (依赖 brain, emotion, memory, scheduler)
       │           │
       │           └──→ Tauri Commands & WebSocket
       │
       └──→ main.rs (整合所有模块)

2.3 进程模型

推荐方案:单一进程

Rust 后端与 Tauri 前端运行在同一个进程中,通过 Tauri 的命令系统invoke和事件系统emit进行通信。这种方式的优势

  1. 零 IPC 开销: 无需跨进程通信,所有状态共享
  2. 简化部署: 单一二进制,更容易分发
  3. 原子操作: 避免多进程间的状态同步问题
  4. Rust 所有权优势: 可以在编译时保证线程安全

3. Rust 后端模块设计

3.1 emotion.rs — 情绪引擎

3.1.1 设计目标

Rust 实现的情绪引擎,需要保持与原 Python 版本相同的行为:

  • 五态情绪系统idle / happy / focused / annoyed / sleepy
  • Softmax 概率分布 + 蒙特卡洛采样
  • 驻留时间dwell time机制
  • ContextBoost 事件增益

3.1.2 数据结构

/// 情绪状态枚举
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EmotionState {
    Idle,
    Happy,
    Focused,
    Annoyed,
    Sleepy,
}

impl EmotionState {
    pub fn as_str(&self) -> &'static str {
        match self {
            EmotionState::Idle => "idle",
            EmotionState::Happy => "happy",
            EmotionState::Focused => "focused",
            EmotionState::Annoyed => "annoyed",
            EmotionState::Sleepy => "sleepy",
        }
    }
}

/// 情绪引擎内部状态
pub struct EmotionEngine {
    current: EmotionState,
    probabilities: [f32; 5],      // Softmax 概率分布
    dwell_timer: Duration,         // 当前状态驻留时间
    context_boost: f32,            // ContextBoost 增益系数
    last_update: Instant,
}

impl EmotionEngine {
    pub const DWELL_TIME: Duration = Duration::from_secs(5);
    pub const TRANSITION_PROB: f32 = 0.3;  // 状态转换基础概率
}

3.1.3 核心接口

impl EmotionEngine {
    /// 创建新的情绪引擎,默认状态为 Idle
    pub fn new() -> Self;
    
    /// 获取当前情绪状态
    pub fn current(&self) -> EmotionState;
    
    /// 获取情绪概率分布(用于调试/可视化)
    pub fn probabilities(&self) -> &[f32; 5];
    
    /// 更新情绪(基于时间流逝)
    pub fn tick(&mut self, dt: Duration);
    
    /// 应用事件增益ContextBoost
    /// - 正面事件: boost > 1.0
    /// - 负面事件: boost < 1.0
    /// - 中性事件: boost = 1.0
    pub fn apply_event(&mut self, event_type: &str, boost: f32);
    
    /// 获取状态转换为某情绪的概率
    pub fn transition_prob(&self, target: EmotionState) -> f32;
    
    /// 采样下一个状态Softmax + 蒙特卡洛)
    fn sample_next_state(&self) -> EmotionState;
    
    /// 获取当前情绪的动画名称
    pub fn animation_name(&self) -> &'static str {
        match self.current {
            EmotionState::Idle => "idle_0",
            EmotionState::Happy => "happy_0",
            EmotionState::Focused => "focused_0",
            EmotionState::Annoyed => "annoyed_0",
            EmotionState::Sleepy => "sleepy_0",
        }
    }
}

3.1.4 Softmax + 蒙特卡洛采样实现

fn softmax(probs: &[f32]) -> Vec<f32> {
    let max = probs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
    let exp: Vec<f32> = probs.iter().map(|p| (p - max).exp()).collect();
    let sum: f32 = exp.iter().sum();
    exp.iter().map(|e| e / sum).collect()
}

fn monte_carlo_sample(probs: &[f32]) -> usize {
    let mut rng = rand::thread_rng();
    let u: f32 = rng.gen_range(0.0..1.0);
    let mut cumsum = 0.0_f32;
    for (i, p) in probs.iter().enumerate() {
        cumsum += p;
        if u <= cumsum {
            return i;
        }
    }
    probs.len() - 1
}

3.1.5 与原版差异

方面 原版 (Python) 新版 (Rust)
随机数 numpy.random rand crate
状态存储 类属性 结构体字段
概率计算 numpy softmax 手动实现
时间精度 time.time() Instant + Duration

3.2 memory.rs — 记忆系统

3.2.1 设计目标

保持与原版相同的数据结构:

  • SQLite 作为持久化存储
  • float32 向量作为语义编码
  • cosine 相似度检索
  • RAG 上下文检索能力

3.2.2 数据结构

/// 记忆条目
#[derive(Debug, Clone)]
pub struct MemoryEntry {
    pub id: i64,                      // SQLite rowid
    pub content: String,              // 原始文本内容
    pub embedding: Vec<f32>,          // float32 向量
    pub timestamp: i64,               // Unix timestamp (秒)
    pub emotion_tag: Option<String>,   // 情绪标签(可选)
    pub importance: f32,              // 重要性评分 0.0-1.0
}

/// 记忆系统配置
#[derive(Debug, Clone)]
pub struct MemoryConfig {
    pub db_path: PathBuf,
    pub embedding_dim: usize,         // 向量维度,默认 384
    pub max_entries: usize,           // 最大记忆条目数
    pub similarity_threshold: f32,    // 相似度阈值,默认 0.7
}

3.2.3 Embedder 接口设计(策略模式)

/// Embedder trait - 支持多种嵌入后端
pub trait Embedder: Send + Sync {
    /// 生成文本嵌入向量
    fn embed(&self, text: &str) -> Result<Vec<f32>, EmbedError>;
    
    /// 获取嵌入维度
    fn dimension(&self) -> usize;
    
    /// 获取嵌入器名称
    fn name(&self) -> &'static str;
}

/// 三种内置嵌入实现
pub struct TfidfEmbedder { /* TF-IDF 实现 */ }
pub struct OllamaEmbedder { /* Ollama embeddings API */ }
pub struct OpenAIEmbedder { /* OpenAI embeddings API */ }

impl Embedder for TfidfEmbedder { /* ... */ }
impl Embedder for OllamaEmbedder { /* ... */ }
impl Embedder for OpenAIEmbedder { /* ... */ }

3.2.4 核心接口

pub struct MemorySystem {
    conn: SqliteConnection,
    embedder: Arc<dyn Embedder>,
    config: MemoryConfig,
}

impl MemorySystem {
    /// 初始化记忆系统
    pub async fn new(config: MemoryConfig) -> Result<Self, MemoryError>;
    
    /// 添加新记忆
    pub async fn add(&self, content: &str, emotion_tag: Option<&str>) -> Result<i64, MemoryError>;
    
    /// 检索相似记忆RAG
    /// 返回与 query 最相似的 top_k 条记忆
    pub async fn retrieve(&self, query: &str, top_k: usize) -> Result<Vec<MemoryEntry>, MemoryError>;
    
    /// 基于向量相似度检索
    pub async fn retrieve_by_vector(&self, query_vec: &[f32], top_k: usize) -> Result<Vec<MemoryEntry>, MemoryError>;
    
    /// 构建 RAG 上下文
    pub fn build_rag_context(&self, entries: &[MemoryEntry]) -> String {
        entries
            .iter()
            .map(|e| format!("[{}] {}", e.timestamp, e.content))
            .collect::<Vec<_>>()
            .join("\n")
    }
    
    /// 删除记忆
    pub async fn delete(&self, id: i64) -> Result<(), MemoryError>;
    
    /// 清理旧记忆(按时间或数量)
    pub async fn prune(&self) -> Result<usize, MemoryError>;
}

3.2.5 SQLite schema

CREATE TABLE IF NOT EXISTS memories (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    content TEXT NOT NULL,
    embedding BLOB NOT NULL,
    timestamp INTEGER NOT NULL,
    emotion_tag TEXT,
    importance REAL DEFAULT 0.5
);

CREATE INDEX IF NOT EXISTS idx_timestamp ON memories(timestamp);
CREATE INDEX IF NOT EXISTS idx_emotion ON memories(emotion_tag);

3.2.6 Cosine 相似度计算

pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
    let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
    let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
    let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
    
    if norm_a == 0.0 || norm_b == 0.0 {
        return 0.0;
    }
    dot / (norm_a * norm_b)
}

3.3 brain.rs — 大脑/决策引擎

3.3.1 设计目标

  • 策略模式 LLM 后端Ollama/OpenAI/Dummy
  • think() 作为核心入口
  • RAG 上下文注入
  • Action 标签解析 [ACTION:type:desc]
  • 主动行为决策 _decide_proactive_action

3.3.2 LLM Provider trait策略模式

/// LLM 提供者接口
pub trait LLMProvider: Send + Sync {
    /// 发送对话请求
    async fn chat(&self, messages: &[ChatMessage]) -> Result<String, LLMError>;
    
    /// 获取提供者名称
    fn name(&self) -> &'static str;
    
    /// 检查提供者是否可用
    async fn health_check(&self) -> bool;
}

/// 聊天消息结构
#[derive(Debug, Clone)]
pub struct ChatMessage {
    pub role: MessageRole,
    pub content: String,
}

#[derive(Debug, Clone, Copy)]
pub enum MessageRole {
    System,
    User,
    Assistant,
}

/// 三种 LLM 提供者实现
pub struct OllamaProvider { /* Ollama API 实现 */ }
pub struct OpenAIProvider { /* OpenAI API 实现 */ }
pub struct DummyProvider { /* 模拟响应,用于测试 */ }

impl LLMProvider for OllamaProvider { /* ... */ }
impl LLMProvider for OpenAIProvider { /* ... */ }
impl LLMProvider for DummyProvider { /* ... */ }

3.3.3 核心接口

pub struct AgentBrain {
    provider: Arc<dyn LLMProvider>,
    memory: Arc<MemorySystem>,
    emotion: Arc<RwLock<EmotionEngine>>,
    config: BrainConfig,
}

#[derive(Debug, Clone)]
pub struct BrainConfig {
    pub system_prompt: String,
    pub max_context_length: usize,
    pub temperature: f32,
    pub use_rag: bool,
    pub rag_top_k: usize,
}

impl AgentBrain {
    /// 创建大脑实例
    pub fn new(provider: Arc<dyn LLMProvider>, memory: Arc<MemorySystem>, emotion: Arc<RwLock<EmotionEngine>>) -> Self;
    
    /// 核心思考入口
    pub async fn think(&self, user_input: &str) -> Result<BrainResponse, BrainError>;
    
    /// 主动行为决策
    pub async fn decide_proactive_action(&self) -> Option<ProactiveAction>;
    
    /// 解析 Action 标签
    fn parse_action(response: &str) -> Option<Action>;
    
    /// 构建带有 RAG 上下文的 prompt
    fn build_rag_prompt(&self, user_input: &str) -> String;
}

/// 决策响应
#[derive(Debug)]
pub struct BrainResponse {
    pub text: String,                 // 原始回复文本
    pub action: Option<Action>,       // 解析出的 Action若有
    pub emotion_delta: Option<f32>,   // 情绪变化量
}

/// Action 结构
#[derive(Debug, Clone)]
pub struct Action {
    pub action_type: ActionType,
    pub description: String,
}

#[derive(Debug, Clone, Copy)]
pub enum ActionType {
    // 移动类
    MoveTo,        // 移动到位置
    Wander,        // 随机漫步
    
    // 表情类
    Express,       // 表情变化
    Animate,       // 播放动画
    
    // 交互类
    Speak,         // 说话
    Listen,        // 倾听
    Ignore,        // 忽略
    
    // 状态类
    Sleep,         // 进入睡眠
    WakeUp,        // 唤醒
    Idle,          // 待机
}

3.3.4 Action 标签解析

/// 解析 [ACTION:type:desc] 格式的标签
/// 例如: "好的主人![ACTION:move_to:center:移动到屏幕中央]"
fn parse_action(text: &str) -> Option<(String, Action)> {
    let re = Regex::new(r"\[ACTION:(\w+):([^\]]+)\]").ok()?;
    let caps = re.captures(text)?;
    
    let action_type = caps.get(1)?.as_str();
    let description = caps.get(2)?.as_str();
    
    let action = match action_type {
        "move_to" => ActionType::MoveTo,
        "wander" => ActionType::Wander,
        "express" => ActionType::Express,
        "animate" => ActionType::Animate,
        "speak" => ActionType::Speak,
        "sleep" => ActionType::Sleep,
        "idle" => ActionType::Idle,
        _ => return None,
    };
    
    Some((text.replace(&caps[0], "").trim().to_string(), Action { action_type: action, description: description.to_string() }))
}

3.3.5 系统提示词模板

fn default_system_prompt(emotion_state: &str) -> String {
    format!(r#"你是一只可爱的桌面宠物,名字叫 EzVibe。
当前情绪状态: {}
请根据用户的输入做出自然的回应。

## 可用动作
你可以在回复末尾添加 [ACTION:type:description] 来触发动作。
例如: "好的呀![ACTION:move_to:center:开心地跳到屏幕中央]"

## 注意事项
1. 保持回复简短有趣(不超过 50 字)
2. 根据情绪状态调整回复风格
3. 不要频繁触发动作(概率 < 30%"#, emotion_state)
}

3.4 scheduler.rs — 任务调度器

3.4.1 设计目标

  • 优先级队列P0/P1/P2/P3
  • 冷却时间管理
  • 概率触发机制
  • 情绪调制概率
  • 活跃度阈值

3.4.2 优先级定义

优先级 名称 说明 典型场景
P0 紧急 最高优先级,可打断任何操作 健康提醒、用户显式请求
P1 用户输入 用户主动交互 聊天、点击、拖拽
P2 闲聊 自然的闲聊触发 定时问候、随机搭话
P3 自触发 LLM 主动发起的动作 宠物主动探索、情绪表达

3.4.3 数据结构

/// 调度任务
#[derive(Debug, Clone)]
pub struct ScheduledTask {
    pub id: TaskId,
    pub priority: Priority,
    pub payload: TaskPayload,
    pub cooldown: Duration,            // 冷却时间
    pub last_trigger: Option<Instant>, // 上次触发时间
    pub probability: f32,              // 基础触发概率
    pub emotion_modulator: fn(EmotionState) -> f32,  // 情绪调制函数
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Priority {
    P0 = 0,  // 紧急
    P1 = 1,  // 用户输入
    P2 = 2,  // 闲聊
    P3 = 3,  // 自触发
}

pub enum TaskPayload {
    HealthReminder,      // P0: 健康提醒
    UserInteraction,      // P1: 用户交互
    CasualChat,           // P2: 闲聊
    ProactiveAction,     // P3: 主动行为
}

/// 调度器配置
#[derive(Debug, Clone)]
pub struct SchedulerConfig {
    pub check_interval: Duration,      // 检查间隔,默认 10s
    pub idle_threshold: Duration,      // 空闲阈值,默认 60s
    pub max_tasks_per_cycle: usize,    // 每周期最大任务数
}

3.4.4 核心接口

pub struct TaskScheduler {
    tasks: RwLock<Vec<ScheduledTask>>,
    emotion: Arc<RwLock<EmotionEngine>>,
    config: SchedulerConfig,
    running: AtomicBool,
}

impl TaskScheduler {
    /// 创建调度器
    pub fn new(config: SchedulerConfig, emotion: Arc<RwLock<EmotionEngine>>) -> Self;
    
    /// 注册任务
    pub fn register(&self, task: ScheduledTask) -> TaskId;
    
    /// 取消任务
    pub fn unregister(&self, id: TaskId) -> bool;
    
    /// 获取下一个可执行的任务(阻塞)
    pub async fn next_task(&self) -> Option<(TaskId, TaskPayload)>;
    
    /// 标记任务完成(更新冷却)
    pub fn mark_complete(&self, id: TaskId);
    
    /// 计算任务的情绪调制概率
    fn calc_probability(&self, task: &ScheduledTask) -> f32 {
        let base = task.probability;
        let emotion_state = self.emotion.read().unwrap().current();
        let modulator = (task.emotion_modulator)(emotion_state);
        base * modulator
    }
    
    /// 启动调度循环
    pub async fn run(&self, action_sender: mpsc::Sender<Action>);
    
    /// 停止调度器
    pub fn stop(&self);
}

3.4.5 情绪调制函数

/// 默认情绪调制函数
fn default_emotion_modulator(state: EmotionState) -> f32 {
    match state {
        EmotionState::Idle => 1.0,      // 正常概率
        EmotionState::Happy => 1.2,     // 更活跃,更容易触发
        EmotionState::Focused => 0.8,   // 专注时减少主动行为
        EmotionState::Annoyed => 0.6,   // 烦躁时减少互动
        EmotionState::Sleepy => 0.3,   // 困倦时大幅减少
    }
}

3.4.6 冷却管理

impl ScheduledTask {
    pub fn is_cooling_down(&self) -> bool {
        match self.last_trigger {
            Some(t) => t.elapsed() < self.cooldown,
            None => false,
        }
    }
    
    pub fn remaining_cooldown(&self) -> Duration {
        match self.last_trigger {
            Some(t) => self.cooldown.saturating_sub(t.elapsed()),
            None => Duration::ZERO,
        }
    }
}

3.5 api.rs — API 与 Tauri 命令入口

3.5.1 设计目标

  • Tauri 命令系统invoke作为前端调用后端的主要方式
  • WebSocket 实时推送(后端 → 前端)
  • 状态查询接口

3.5.2 Tauri Commands

use tauri::command;

/// 获取应用健康状态
#[command]
pub async fn health() -> Result<HealthStatus, String> {
    Ok(HealthStatus {
        status: "ok".to_string(),
        uptime: get_uptime(),
    })
}

/// 发送聊天消息
#[command]
pub async fn chat(message: String, emotion: String) -> Result<ChatResponse, String> {
    let brain = get_brain().await;
    let response = brain.think(&message).await
        .map_err(|e| e.to_string())?;
    
    Ok(ChatResponse {
        text: response.text,
        action: response.action,
        emotion_delta: response.emotion_delta,
    })
}

/// 获取当前情绪状态
#[command]
pub fn get_emotion() -> EmotionStatus {
    let emotion = get_emotion_engine();
    EmotionStatus {
        state: emotion.current().as_str().to_string(),
        probabilities: emotion.probabilities().to_vec(),
        dwell_remaining: emotion.dwell_remaining().as_secs_f32(),
    }
}

/// 获取记忆上下文RAG
#[command]
pub async fn get_memory_context(query: String, top_k: usize) -> Result<String, String> {
    let memory = get_memory_system().await;
    let entries = memory.retrieve(&query, top_k).await
        .map_err(|e| e.to_string())?;
    Ok(memory.build_rag_context(&entries))
}

/// 添加新记忆
#[command]
pub async fn add_memory(content: String, emotion_tag: Option<String>) -> Result<i64, String> {
    let memory = get_memory_system().await;
    memory.add(&content, emotion_tag.as_deref()).await
        .map_err(|e| e.to_string())
}

/// 获取调度器状态
#[command]
pub fn get_scheduler_status() -> SchedulerStatus {
    let scheduler = get_scheduler();
    SchedulerStatus {
        running: scheduler.is_running(),
        active_tasks: scheduler.active_count(),
    }
}

3.5.3 WebSocket 事件推送

/// WebSocket 事件类型
#[derive(Debug, Clone, Serialize)]
pub enum WsEvent {
    EmotionChange { state: String, animation: String },
    ActionTrigger { action: Action },
    Heartbeat { timestamp: i64 },
    StateSync { full_state: AppState },
    ConnectionStatus { connected: bool },
}

impl WsEvent {
    pub fn event_type(&self) -> &'static str {
        match self {
            WsEvent::EmotionChange { .. } => "emotion_change",
            WsEvent::ActionTrigger { .. } => "action",
            WsEvent::Heartbeat { .. } => "heartbeat",
            WsEvent::StateSync { .. } => "state_sync",
            WsEvent::ConnectionStatus { .. } => "connected",
        }
    }
}

/// 事件广播器
pub struct EventBroadcaster {
    connections: RwLock<Vec<WebSocketChannel>>,
}

impl EventBroadcaster {
    /// 广播情绪变化
    pub fn broadcast_emotion_change(&self, state: EmotionState) {
        let event = WsEvent::EmotionChange {
            state: state.as_str().to_string(),
            animation: state.animation_name().to_string(),
        };
        self.broadcast(event);
    }
    
    /// 广播主动行为
    pub fn broadcast_action(&self, action: Action) {
        let event = WsEvent::ActionTrigger { action };
        self.broadcast(event);
    }
}

3.5.4 AppState 单例

/// 应用全局状态(替代 Python 的 AppState 单例)
pub struct AppState {
    pub emotion: Arc<RwLock<EmotionEngine>>,
    pub memory: Arc<MemorySystem>,
    pub brain: Arc<AgentBrain>,
    pub scheduler: Arc<TaskScheduler>,
    pub broadcaster: Arc<EventBroadcaster>,
    pub config: Arc<Config>,
}

lazy_static::lazy_static! {
    pub static ref APP_STATE: AppState = AppState::new();
}

impl AppState {
    fn new() -> Self {
        Self {
            emotion: Arc::new(RwLock::new(EmotionEngine::new())),
            memory: Arc::new(MemorySystem::new()),
            brain: Arc::new(AgentBrain::new()),
            scheduler: Arc::new(TaskScheduler::new()),
            broadcaster: Arc::new(EventBroadcaster::new()),
            config: Arc::new(Config::load()),
        }
    }
}

3.6 perception.rs — 感知模块(保留与否讨论)

3.6.1 原版功能回顾

原 Python 版本的 perception 模块主要负责:

  • 键盘/鼠标活动检测
  • 用户空闲状态判断
  • 系统时间/日期感知

3.6.2 重构决策

推荐方案:移除 perception 模块

原因:

  1. Tauri 负责窗口操作: 用户明确表示 KeyboardMouseMonitor 不重构Tauri 自己负责拖拽
  2. 窗口管理属于前端: 拖拽、点击检测等由 Tauri/前端处理更合适
  3. 简化后端职责: Rust 后端专注于 AI 逻辑,不涉及系统感知
  4. 性能考量: Rust 的系统级感知能力强大,但在这个场景下不需要

3.6.3 替代方案

如果确实需要感知能力,可通过以下方式实现:

/// 前端感知事件(通过 Tauri 事件转发)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerceptionEvent {
    pub event_type: PerceptionType,
    pub timestamp: i64,
    pub data: serde_json::Value,
}

#[derive(Debug, Clone, Copy)]
pub enum PerceptionType {
    UserActive,     // 用户活动
    UserIdle,       // 用户空闲
    TimeEvent,      // 时间事件(整点报时等)
    FocusChange,    // 窗口焦点变化
}

/// 前端通过 WebSocket 发送感知事件
/// Rust 后端通过 Tauri event listener 接收

总结感知逻辑应该在前端实现Rust 后端只接收已处理的事件数据。


3.7 main.rs — 启动流程与初始化

3.7.1 启动流程

┌─────────────────────────────────────────────────────────┐
│                     main()                              │
├─────────────────────────────────────────────────────────┤
│ 1. 加载配置文件 (config.toml)                           │
│ 2. 初始化日志系统                                        │
│ 3. 创建全局 AppState                                    │
│ 4. 初始化 EmotionEngine                                 │
│ 5. 初始化 MemorySystem加载/创建 SQLite              │
│ 6. 创建 LLM ProviderOllama 或 OpenAI               │
│ 7. 初始化 AgentBrain                                    │
│ 8. 启动 TaskScheduler                                   │
│ 9. 启动 WebSocket Server用于实时推送                │
│ 10. 注册 Tauri Commands                                 │
│ 11. 进入 Tauri 主循环                                   │
└─────────────────────────────────────────────────────────┘

3.7.2 代码结构

mod config;
mod emotion;
mod memory;
mod brain;
mod scheduler;
mod api;

use std::sync::Arc;
use tauri::Manager;

mod main {
    use super::*;
    
    fn main() {
        // 初始化日志
        init_logging();
        
        // 加载配置
        let config = Config::load().expect("Failed to load config");
        
        // 创建全局状态
        let app_state = Arc::new(AppState::new(&config));
        
        // 构建 Tauri 应用
        tauri::Builder::default()
            .manage(app_state)
            .setup(|app| {
                // 初始化所有模块
                init_modules(app)?;
                Ok(())
            })
            .invoke_handler(tauri::generate_handler![
                api::health,
                api::chat,
                api::get_emotion,
                api::get_memory_context,
                api::add_memory,
                api::get_scheduler_status,
            ])
            .run(tauri::generate_context!())
            .expect("Failed to run Tauri application");
    }
    
    fn init_modules(app: &tauri::App) -> Result<(), Box<dyn Error>> {
        let state = app.state::<Arc<AppState>>();
        
        // 初始化记忆系统
        state.memory.init().await?;
        
        // 初始化大脑(创建 LLM provider
        state.brain.init().await?;
        
        // 启动调度器
        state.scheduler.start().await?;
        
        // 启动 WebSocket 广播
        state.broadcaster.start()?;
        
        log::info!("All modules initialized successfully");
        Ok(())
    }
}

3.7.3 初始化顺序图

config.toml
     │
     ▼
┌─────────────┐
│ load_config │
└─────────────┘
     │
     ▼
┌─────────────┐
│ EmotionEngine│
│ (无依赖)     │
└─────────────┘
     │
     ├──────────────────┬────────────────────┐
     ▼                  ▼                    ▼
┌─────────────┐  ┌─────────────┐    ┌─────────────┐
│ MemorySystem│  │ AgentBrain  │    │ TaskScheduler│
│ (无依赖)    │  │ (依赖Mem)   │    │ (依赖Emotion)│
└─────────────┘  └─────────────┘    └─────────────┘
     │                  │                    │
     └──────────┬───────┘                    │
                │                            │
                ▼                            │
         ┌─────────────┐                     │
         │   API       │◄────────────────────┘
         │ (所有模块)  │
         └─────────────┘
                │
                ▼
         ┌─────────────┐
         │ Tauri App   │
         └─────────────┘

4. Tauri 前端设计

4.1 窗口配置

4.1.1 tauri.conf.json 配置

{
  "build": {
    "devtools": true
  },
  "app": {
    "windows": [
      {
        "title": "EzVibe",
        "width": 300,
        "height": 300,
        "resizable": false,
        "decorations": false,
        "transparent": true,
        "always_on_top": true,
        "skip_taskbar": true,
        "center": true,
        "visible": true,
        "shadow": false
      }
    ]
  },
  "tauri": {
    "dragDropEnabled": true
  }
}

4.1.2 窗口特性说明

配置项 说明
decorations false 无边框窗口
transparent true 透明背景
always_on_top true 置顶显示
resizable false 不可调整大小
skip_taskbar true 不显示在任务栏
shadow false 无阴影(避免干扰透明效果)

4.1.3 拖拽实现

// React 组件实现拖拽
import { useWindow } from '@tauri-apps/api/window';

const PetWindow = () => {
  const appWindow = useWindow();
  
  const handleMouseDown = async (e: React.MouseEvent) => {
    if (e.button === 0) { // 左键拖拽
      await appWindow.startDragging();
    }
  };
  
  return (
    <div 
      className="pet-container"
      onMouseDown={handleMouseDown}
      style={{ 
        width: '100%', 
        height: '100%',
        cursor: 'move' 
      }}
    >
      {/* Live2D Canvas */} 
    </div>
  );
};

4.2 Live2D Canvas 集成

4.2.1 技术选型

使用 live2d-cubism-core + @pixi/react-live2d 或原生 Canvas 实现。

4.2.2 集成方案

// Live2DViewer.tsx
import { useEffect, useRef } from 'react';
import * as LIVE2D from 'live2dcubismcore';

interface Live2DViewerProps {
  modelPath: string;
  animationName: string;
  emotionState: string;
}

const Live2DViewer: React.FC<Live2DViewerProps> = ({ 
  modelPath, 
  animationName,
  emotionState 
}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const modelRef = useRef<LIVE2D.Live2DModel>();
  
  useEffect(() => {
    // 加载模型
    LIVE2D.Live2DModel.fromUrl(modelPath).then((model) => {
      modelRef.current = model;
      
      // 初始化渲染
      const gl = canvasRef.current?.getContext('webgl2');
      if (gl) {
        model.setGL(gl);
        model.drawables.meshes.forEach((mesh) => {
          mesh.setTexture(model.textures[0]);
        });
      }
    });
  }, [modelPath]);
  
  // 监听动画变化
  useEffect(() => {
    if (modelRef.current) {
      modelRef.current.internalModel.motionManager.startMotion(animationName);
    }
  }, [animationName]);
  
  // 监听情绪状态变化(可选的微妙动画调整)
  useEffect(() => {
    if (modelRef.current) {
      // 根据情绪调整眼部表情、嘴巴开合等
      const expressionMap: Record<string, string> = {
        idle: 'idle',
        happy: 'happy',
        focused: 'focused',
        annoyed: 'annoyed',
        sleepy: 'sleepy',
      };
      modelRef.current.internalModel.setExpression(expressionMap[emotionState]);
    }
  }, [emotionState]);
  
  return (
    <canvas 
      ref={canvasRef} 
      style={{ 
        width: '100%', 
        height: '100%',
        pointerEvents: 'auto' // 允许接收点击事件用于拖拽
      }} 
    />
  );
};

export default Live2DViewer;

4.2.3 模型资源组织

src-tauri/
└── resources/
    └── models/
        └── ezvibe/
            ├── model.json          # Live2D 模型配置
            ├── model.moc3         # 模型数据
            ├── textures/          # 纹理图集
            │   ├── texture_00.png
            │   └── texture_01.png
            └── motions/           # 动作文件
                ├── idle.motion3.json
                ├── happy.motion3.json
                └── ...

4.3 WebSocket 客户端

4.3.1 连接管理

// useWebSocket.ts
import { useEffect, useRef, useState } from 'react';

interface UseWebSocketOptions {
  url: string;
  onMessage: (event: WsEvent) => void;
  onConnect?: () => void;
  onDisconnect?: () => void;
}

export const useWebSocket = ({ 
  url, 
  onMessage, 
  onConnect, 
  onDisconnect 
}: UseWebSocketOptions) => {
  const [connected, setConnected] = useState(false);
  const wsRef = useRef<WebSocket | null>(null);
  
  useEffect(() => {
    const ws = new WebSocket(url);
    wsRef.current = ws;
    
    ws.onopen = () => {
      setConnected(true);
      onConnect?.();
    };
    
    ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        onMessage(data);
      } catch (e) {
        console.error('Failed to parse WebSocket message:', e);
      }
    };
    
    ws.onclose = () => {
      setConnected(false);
      onDisconnect?.();
    };
    
    return () => {
      ws.close();
    };
  }, [url]);
  
  return { connected };
};

4.3.2 事件处理

// App.tsx
import { invoke } from '@tauri-apps/api/tauri';
import { useWebSocket } from './hooks/useWebSocket';

const App = () => {
  const [emotionState, setEmotionState] = useState('idle');
  const [animationName, setAnimationName] = useState('idle_0');
  
  const handleWsMessage = (event: WsEvent) => {
    switch (event.type) {
      case 'emotion_change':
        setEmotionState(event.state);
        setAnimationName(event.animation);
        break;
      case 'action':
        // 处理宠物动作指令
        handleAction(event.action);
        break;
      case 'state_sync':
        // 完整状态同步
        setEmotionState(event.full_state.emotion);
        break;
    }
  };
  
  const ws = useWebSocket({
    url: 'ws://localhost:9222/ws', // 本地 WebSocket
    onMessage: handleWsMessage,
  });
  
  return (
    <div className="pet-window">
      <Live2DViewer
        emotionState={emotionState}
        animationName={animationName}
      />
    </div>
  );
};

4.4 前端无业务逻辑原则

核心原则:前端只负责渲染和转发,不处理任何业务逻辑。

前端职责 后端职责
显示 Live2D 动画 计算情绪状态
响应用户拖拽 管理任务调度
显示对话气泡 调用 LLM 决策
转发用户输入到后端 存储记忆数据
显示宠物状态 解析 Action 标签
// 正确示例:前端只做转发
const handleUserInput = async (message: string) => {
  const response = await invoke<ChatResponse>('chat', { 
    message, 
    emotion: currentEmotion 
  });
  // 仅显示响应,不做业务判断
  showDialogBubble(response.text);
  if (response.action) {
    triggerAction(response.action);
  }
};

// 错误示例:前端包含业务逻辑
const handleUserInput = async (message: string) => {
  // ❌ 不应该在前端做情绪判断
  const emotion = message.includes('开心') ? 'happy' : 'idle';
  const response = await invoke('chat', { message, emotion });
  // ...
};

5. 通信协议设计

5.1 Tauri invoke 命令(前端 → 后端)

5.1.1 命令定义

命令 参数 返回值 说明
health - HealthStatus 健康检查
chat message: string, emotion: string ChatResponse 发送聊天消息
get_emotion - EmotionStatus 获取当前情绪
get_memory_context query: string, top_k: number string 获取 RAG 上下文
add_memory content: string, emotion_tag?: string number 添加记忆
get_scheduler_status - SchedulerStatus 获取调度器状态
update_config key: string, value: string boolean 更新配置
reset_emotion - boolean 重置情绪状态

5.1.2 请求/响应类型

// 前端 TypeScript 类型定义
interface HealthStatus {
  status: 'ok' | 'error';
  uptime: number; // 秒
}

interface ChatResponse {
  text: string;
  action?: Action;
  emotion_delta?: number;
}

interface Action {
  action_type: string;
  description: string;
}

interface EmotionStatus {
  state: string; // 'idle' | 'happy' | 'focused' | 'annoyed' | 'sleepy'
  probabilities: number[];
  dwell_remaining: number; // 秒
}

interface SchedulerStatus {
  running: boolean;
  active_tasks: number;
}

5.2 WebSocket 消息(后端 → 前端实时推送)

5.2.1 消息格式

{
  "type": "emotion_change",
  "timestamp": 1716825600,
  "data": {
    "state": "happy",
    "animation": "happy_0"
  }
}

5.2.2 消息类型

类型 说明 典型场景
emotion_change 情绪状态变化 宠物情绪转变
action 主动行为触发 宠物移动、说话
heartbeat 心跳保活 每 30s 一次
state_sync 完整状态同步 连接建立时
connected 连接状态变化 连接/断开

5.2.3 消息示例

// 情绪变化
{
  "type": "emotion_change",
  "timestamp": 1716825600,
  "data": {
    "state": "happy",
    "animation": "happy_0"
  }
}

// 动作触发
{
  "type": "action",
  "timestamp": 1716825600,
  "data": {
    "action_type": "move_to",
    "description": "开心地跳到屏幕中央",
    "target": { "x": 0.5, "y": 0.5 }
  }
}

// 心跳
{
  "type": "heartbeat",
  "timestamp": 1716825600,
  "data": {}
}

// 状态同步
{
  "type": "state_sync",
  "timestamp": 1716825600,
  "data": {
    "emotion": {
      "state": "idle",
      "probabilities": [0.4, 0.3, 0.1, 0.1, 0.1]
    },
    "scheduler": {
      "running": true,
      "active_tasks": 2
    }
  }
}

5.3 REST HTTP可选的 HTTP fallback

5.3.1 端点列表

方法 路径 说明
GET /health 健康检查
POST /chat 发送消息
GET /state 获取完整状态
GET /emotion 获取情绪状态
PUT /emotion 修改情绪
GET /memory 列出记忆
POST /memory 添加记忆
DELETE /memory/:id 删除记忆

5.3.2 使用场景

  • Web 管理界面: 可选的管理面板
  • 调试工具: 开发时用于调试
  • 第三方集成: 允许其他应用查询宠物状态

注意: REST 接口作为可选功能,主要通信仍通过 Tauri invoke + WebSocket 完成。


6. 数据流图

6.1 用户交互完整链路

┌──────────────────────────────────────────────────────────────────────────┐
│                           用户交互数据流                                  │
└──────────────────────────────────────────────────────────────────────────┘

[1. 用户输入]
     │
     ▼
┌─────────────┐      ┌─────────────┐
│   前端      │      │   输入验证  │
│  React UI   │─────►│             │
└─────────────┘      └──────┬──────┘
                            │
[2. Tauri invoke]           │
     │                      │
     ▼                      ▼
┌─────────────┐      ┌─────────────┐
│   Rust      │      │  参数校验   │
│  Commands   │◄─────│             │
└──────┬──────┘      └─────────────┘
       │
       │
[3. 业务处理]
       │
       ▼
┌─────────────────────────────────────────────────────────────────┐
│                      AgentBrain.think()                        │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐                    │
│  │ Memory  │    │ Emotion │    │   LLM   │                    │
│  │ System  │    │ Engine  │    │ Provider│                    │
│  └────┬────┘    └────┬────┘    └────┬────┘                    │
│       │              │              │                         │
│       │              │              │                         │
│       └──────────────┼──────────────┘                         │
│                      │                                        │
│                      ▼                                        │
│              ┌───────────────┐                                │
│              │  BrainResponse│                                │
│              │  - text       │                                │
│              │  - action     │                                │
│              │  - emotion_delta│                               │
│              └───────┬───────┘                                │
└──────────────────────┼───────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────────────────┐
│                     响应处理                                     │
│                                                                 │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐     │
│  │ 文本响应     │    │ Action 解析  │    │ 情绪更新     │     │
│  │              │    │              │    │              │     │
│  │ 返回给前端   │    │ 更新状态     │    │ 触发事件     │     │
│  └──────┬───────┘    └──────┬───────┘    └──────┬───────┘     │
│         │                   │                   │              │
└─────────┼───────────────────┼───────────────────┼──────────────┘
          │                   │                   │
          ▼                   ▼                   ▼
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│  响应消息   │      │ Action执行  │      │ WebSocket   │
│  返回前端   │      │ 更新Pet位置 │      │ 广播事件   │
└─────────────┘      └─────────────┘      └─────────────┘

6.2 主动行为触发链路

┌─────────────────────────────────────────────────────────────────┐
│                    主动行为触发链路                              │
└─────────────────────────────────────────────────────────────────┘

[调度器定时检查]
        │
        ▼
┌─────────────────┐
│ TaskScheduler   │
│ .check_tasks()  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐     ┌─────────────────┐
│ 冷却检查        │────►│ 跳过冷却中任务  │
└────────┬────────┘     └─────────────────┘
         │
         │ 未冷却
         ▼
┌─────────────────┐
│ 概率触发        │
│ (基础 × 情绪)  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ Brain.decide_   │─────► AgentBrain.think()
│ proactive_action│     (可选择性的调用)
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ Action 生成     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐     ┌─────────────────┐
│ WebSocket       │────►│ 前端接收并执行  │
│ 广播            │     │ Live2D 动画     │
└─────────────────┘     └─────────────────┘

6.3 记忆检索链路RAG

┌─────────────────────────────────────────────────────────────────┐
│                     RAG 记忆检索链路                            │
└─────────────────────────────────────────────────────────────────┘

User Input
    │
    ▼
┌─────────────────┐
│ Embedder.embed() │
│ (query 向量化)   │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ MemorySystem    │
│ .retrieve()     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐     ┌─────────────────┐
│ Cosine 相似度  │────►│ 过滤 < threshold │
│ 计算            │     └─────────────────┘
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 返回 Top-K      │
│ MemoryEntries   │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ build_rag_context│
│ 拼接上下文      │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ 注入 Prompt     │
│ 发送给 LLM      │
└─────────────────┘

7. 进程模型讨论

7.1 方案对比

方案 A单一进程推荐

┌────────────────────────────────────────────────┐
│              Tauri Application                 │
│  ┌────────────────┐  ┌────────────────┐      │
│  │   Frontend      │  │    Backend     │      │
│  │   (React/Web)   │◄─►│   (Rust)       │      │
│  └────────────────┘  └────────────────┘      │
│         │                    │                │
│         │     Shared State   │                │
│         │◄──────────────────►│                │
│         │                    │                │
└─────────┼────────────────────┼────────────────┘
          │                    │
          ▼                    ▼
     ┌─────────┐        ┌──────────┐
     │ Live2D  │        │  SQLite  │
     │ Canvas  │        │  Local   │
     └─────────┘        └──────────┘
优点 缺点
零 IPC 开销 前端崩溃会导致整个应用退出
状态共享简单 内存占用集中
部署简单 调试相对复杂
原子操作无竞争 -

方案 B多进程备选

┌────────────────────────┐     ┌────────────────────────┐
│    Tauri Renderer      │     │      Tauri Main        │
│  ┌──────────────────┐ │     │  ┌──────────────────┐   │
│  │   Frontend        │ │     │  │   Backend        │   │
│  │   (React/Web)     │ │     │  │   (Rust)         │   │
│  └──────────────────┘ │     │  └────────┬─────────┘   │
│         │            │     │           │              │
└─────────┼────────────┘     └───────────┼──────────────┘
          │                             │
          │         IPC (invoke)       │
          │◄──────────────────────────►│
          │                             │
          ▼                             ▼
   ┌─────────────┐              ┌─────────────┐
   │  Live2D     │              │   SQLite   │
   │  Canvas     │              │   Local    │
   └─────────────┘              └─────────────┘
优点 缺点
前端崩溃不影响后端 IPC 开销
隔离性好 状态同步复杂
便于独立调试 部署复杂
- 可能引入竞争条件

7.2 推荐结论

采用方案 A单一进程

理由:

  1. 性能优先: 宠物桌面是轻量应用,不需要进程隔离带来的安全性
  2. 简化状态管理: Rust 的所有权模型和 TypeScript 的强类型可以保证数据一致性
  3. 与 Tauri 架构匹配: Tauri 天然支持前端-后端紧耦合
  4. 降低复杂度: 减少跨进程通信的复杂度

8. Rust 库选型

用途 库名 版本 说明
Web 框架 axum 0.7 轻量、高性能、async
WebSocket tokio-tungstenite 0.21 配合 tokio 使用
SQLite rusqlite 0.31 同步 API简单直接
ORM sqlx 0.7 可选,支持 async
JSON 序列化 serde 1.0 事实标准
异步运行时 tokio 1.0 最流行
随机数 rand 0.8 蒙特卡洛采样
正则表达式 regex 1.0 Action 标签解析
配置管理 config 0.14 TOML 配置文件
日志 tracing 0.1 结构化日志
全局状态 once_cell 1.0 AppState 单例
HTTP 客户端 reqwest 0.11 LLM API 调用
Tauri 集成 tauri 2.0 核心框架
Live2D可选 cubism-core - 如果后端需要处理模型

9. 重构工作量评估

9.1 模块复杂度评估

模块 代码行数(估) 复杂度 优先级
emotion.rs 200-300 P1
memory.rs 400-500 P1
brain.rs 500-600 P1
scheduler.rs 300-400 P2
api.rs 300-400 P2
main.rs 100-150 P1
前端 (React) 800-1000 P2

9.2 工时估算

阶段 内容 工时(人/天)
Phase 1: 核心模块 emotion + memory + main 3-4
Phase 2: 大脑引擎 brain + LLM provider 4-5
Phase 3: 调度器 scheduler 2-3
Phase 4: API 层 Tauri commands + WebSocket 2-3
Phase 5: 前端 React + Live2D 4-5
Phase 6: 集成测试 端到端测试 2-3
Phase 7: 优化 性能调优 1-2
总计 - 18-25

9.3 风险点

  1. Live2D 集成: 前端 Live2D 模型加载和渲染可能遇到兼容性问题
  2. LLM Provider: Ollama/OpenAI API 的错误处理需要完善
  3. 状态同步: WebSocket 连接断开重连的状态恢复

10. 与原 Python 版本的差异对比

方面 原版 (Python/Qt) 新版 (Rust/Tauri)
前端框架 PyQt5 React + Vite
后端语言 Python 3.11+ Rust 1.75+
窗口管理 Qt Window Tauri Webview
进程模型 主线程 + 后台线程 单一进程 + async
动画渲染 QPixmap 帧动画 Live2D Canvas
实时通信 Flask-SocketIO WebSocket (tokio)
HTTP API Flask Axum
数据存储 SQLite + pickle SQLite + serde
向量存储 numpy BLOB 原始 BLOB
日志 logging tracing
配置 dict + JSON config crate
启动时间 ~3-5s < 2s
内存占用 ~150MB < 80MB
CPU 占用(空闲) ~5-10% < 1%
二进制大小 N/A需要 Python < 15MB

10.1 架构变化对比图

原版架构:

┌─────────────────────────────────────────────────┐
│                 main.py                         │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌───────┐  │
│  │ Emotion │ │ Memory  │ │ Brain   │ │Sched- │  │
│  │ Engine  │ │ System  │ │         │ │uler   │  │
│  └────┬────┘ └────┬────┘ └────┬────┘ └───┬───┘  │
│       │           │           │          │      │
│       └───────────┴───────────┴──────────┘      │
│                       │                         │
│               ┌───────┴───────┐                 │
│               │  AppState     │                 │
│               └───────┬───────┘                 │
│                       │                         │
│       ┌───────────────┼───────────────┐       │
│       │               │               │       │
│  ┌────┴────┐    ┌────┴────┐    ┌────┴────┐   │
│  │Flask API │    │Qt Window│    │Scheduler│   │
│  │(Thread)  │    │(Main)   │    │(Thread) │   │
│  └─────────┘    └─────────┘    └─────────┘   │
└─────────────────────────────────────────────────┘

新版架构:

┌─────────────────────────────────────────────────┐
│              Tauri Application                  │
│  ┌────────────────────────────────────────────┐ │
│  │              Rust Backend                  │ │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐     │ │
│  │  │ Emotion │ │ Memory  │ │ Brain   │     │ │
│  │  └────┬────┘ └────┬────┘ └────┬────┘     │ │
│  │       │           │           │          │ │
│  │       └───────────┼───────────┘          │ │
│  │                   │                      │ │
│  │           ┌───────┴───────┐              │ │
│  │           │   AppState    │              │ │
│  │           └───────┬───────┘              │ │
│  │                   │                      │ │
│  │  ┌────────────────┴────────────────┐     │ │
│  │  │         Tauri Commands          │     │ │
│  │  └────────────────┬────────────────┘     │ │
│  └──────────────────┼───────────────────────┘ │
│                     │                          │
│  ┌──────────────────┼───────────────────────┐ │
│  │              React Frontend               │ │
│  │  ┌─────────┐  ┌─────────┐  ┌─────────┐   │ │
│  │  │ Live2D  │  │ WebSocket│  │ State  │   │ │
│  │  │ Canvas  │  │ Client  │  │ Mgmt   │   │ │
│  │  └─────────┘  └─────────┘  └─────────┘   │ │
│  └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘

11. 已识别设计问题的处理方案

11.1 问题 1main.py 调用不存在的 APIServer 类

原问题描述:

main.py调用 api_mod.APIServer() 但 server.py只有 create_app()run_server(),无 APIServer 类

处理方案:

在 Rust 重构中,我们将:

  1. 移除 APIServer 类的概念
  2. 使用 Tauri Commands 作为 API 入口
  3. AppState 单例直接在 Rust 层面实现,保证类型安全
// api.rs - 替代原 APIServer
pub struct ApiLayer {
    state: Arc<AppState>,
}

impl ApiLayer {
    pub fn new(state: Arc<AppState>) -> Self {
        Self { state }
    }
    
    // 所有 API 方法都作为 Tauri Command 实现
}

// main.rs - 正确的初始化方式
fn main() {
    let app_state = Arc::new(AppState::new());
    
    tauri::Builder::default()
        .manage(app_state)  // 直接管理 AppState
        .invoke_handler(tauri::generate_handler![...])
        .run(...);
}

11.2 问题 2DummyPetWindow 接口不一致

原问题描述:

DummyPetWindow.isVisible 是属性而 PetWindow 是方法

处理方案:

在 Rust 重构中,我们将:

  1. 使用 Trait 定义统一的接口
  2. 前端完全移除窗口相关的 Python 代码
  3. Tauri 负责所有窗口操作
// 统一的窗口 TraitRust 后端不直接操作窗口,但保留接口定义)
trait PetWindow: Send {
    fn is_visible(&self) -> bool;
    fn set_position(&mut self, x: f32, y: f32);
    fn play_animation(&mut self, name: &str);
}

前端通过 Tauri 命令控制窗口:

// 前端通过 invoke 调用窗口操作
await invoke('set_window_position', { x: 100, y: 200 });
await invoke('play_animation', { name: 'happy' });

11.3 问题 3asyncio 嵌套调用可能导致状态隔离

原问题描述:

asyncio 嵌套调用可能导致状态隔离问题

处理方案:

在 Rust 重构中,我们将:

  1. 使用单一 tokio runtime避免嵌套
  2. 所有异步操作通过同一个 runtime 执行
  3. 状态通过 Arc<RwLock<T>> 共享
// main.rs - 正确的 async 初始化
#[tokio::main]
async fn main() {
    // 单一 runtime
    let app_state = Arc::new(AppState::new());
    
    tauri::Builder::default()
        .manage(app_state)
        .setup(|app| {
            // 所有初始化在 setup 中完成
            // 不再创建新的 async runtime
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("Error running tauri");
}

关键改进:

  • Python 的 asyncio.run() 在嵌套场景下会创建多个 event loop
  • Rust 的 #[tokio::main] 保证全局单一 runtime
  • 类型系统保证状态不会泄漏到错误的上下文中

12. 附录

12.1 术语表

术语 说明
Tauri 跨平台桌面应用框架,使用 Rust 后端 + Web 前端
Live2D 2D 角色动画技术,支持动态表情和动作
RAG Retrieval-Augmented Generation检索增强生成
Softmax 归一化指数函数,用于概率分布
蒙特卡洛采样 随机采样方法,用于状态选择
Cosine 相似度 向量相似度度量方法
TF-IDF Term Frequency-Inverse Document Frequency词频-逆文档频率

12.2 参考资源


文档结束