# 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 数据结构 ```rust /// 情绪状态枚举 #[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 核心接口 ```rust 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 + 蒙特卡洛采样实现 ```rust fn softmax(probs: &[f32]) -> Vec { let max = probs.iter().cloned().fold(f32::NEG_INFINITY, f32::max); let exp: Vec = 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 数据结构 ```rust /// 记忆条目 #[derive(Debug, Clone)] pub struct MemoryEntry { pub id: i64, // SQLite rowid pub content: String, // 原始文本内容 pub embedding: Vec, // float32 向量 pub timestamp: i64, // Unix timestamp (秒) pub emotion_tag: Option, // 情绪标签(可选) 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 接口设计(策略模式) ```rust /// Embedder trait - 支持多种嵌入后端 pub trait Embedder: Send + Sync { /// 生成文本嵌入向量 fn embed(&self, text: &str) -> Result, 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 核心接口 ```rust pub struct MemorySystem { conn: SqliteConnection, embedder: Arc, config: MemoryConfig, } impl MemorySystem { /// 初始化记忆系统 pub async fn new(config: MemoryConfig) -> Result; /// 添加新记忆 pub async fn add(&self, content: &str, emotion_tag: Option<&str>) -> Result; /// 检索相似记忆(RAG) /// 返回与 query 最相似的 top_k 条记忆 pub async fn retrieve(&self, query: &str, top_k: usize) -> Result, MemoryError>; /// 基于向量相似度检索 pub async fn retrieve_by_vector(&self, query_vec: &[f32], top_k: usize) -> Result, MemoryError>; /// 构建 RAG 上下文 pub fn build_rag_context(&self, entries: &[MemoryEntry]) -> String { entries .iter() .map(|e| format!("[{}] {}", e.timestamp, e.content)) .collect::>() .join("\n") } /// 删除记忆 pub async fn delete(&self, id: i64) -> Result<(), MemoryError>; /// 清理旧记忆(按时间或数量) pub async fn prune(&self) -> Result; } ``` #### 3.2.5 SQLite schema ```sql 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 相似度计算 ```rust 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::().sqrt(); let norm_b: f32 = b.iter().map(|x| x * x).sum::().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(策略模式) ```rust /// LLM 提供者接口 pub trait LLMProvider: Send + Sync { /// 发送对话请求 async fn chat(&self, messages: &[ChatMessage]) -> Result; /// 获取提供者名称 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 核心接口 ```rust pub struct AgentBrain { provider: Arc, memory: Arc, emotion: Arc>, 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, memory: Arc, emotion: Arc>) -> Self; /// 核心思考入口 pub async fn think(&self, user_input: &str) -> Result; /// 主动行为决策 pub async fn decide_proactive_action(&self) -> Option; /// 解析 Action 标签 fn parse_action(response: &str) -> Option; /// 构建带有 RAG 上下文的 prompt fn build_rag_prompt(&self, user_input: &str) -> String; } /// 决策响应 #[derive(Debug)] pub struct BrainResponse { pub text: String, // 原始回复文本 pub action: Option, // 解析出的 Action(若有) pub emotion_delta: Option, // 情绪变化量 } /// 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 标签解析 ```rust /// 解析 [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 系统提示词模板 ```rust 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 数据结构 ```rust /// 调度任务 #[derive(Debug, Clone)] pub struct ScheduledTask { pub id: TaskId, pub priority: Priority, pub payload: TaskPayload, pub cooldown: Duration, // 冷却时间 pub last_trigger: Option, // 上次触发时间 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 核心接口 ```rust pub struct TaskScheduler { tasks: RwLock>, emotion: Arc>, config: SchedulerConfig, running: AtomicBool, } impl TaskScheduler { /// 创建调度器 pub fn new(config: SchedulerConfig, emotion: Arc>) -> 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); /// 停止调度器 pub fn stop(&self); } ``` #### 3.4.5 情绪调制函数 ```rust /// 默认情绪调制函数 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 冷却管理 ```rust 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 ```rust use tauri::command; /// 获取应用健康状态 #[command] pub async fn health() -> Result { Ok(HealthStatus { status: "ok".to_string(), uptime: get_uptime(), }) } /// 发送聊天消息 #[command] pub async fn chat(message: String, emotion: String) -> Result { 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 { 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) -> Result { 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 事件推送 ```rust /// 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>, } 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 单例 ```rust /// 应用全局状态(替代 Python 的 AppState 单例) pub struct AppState { pub emotion: Arc>, pub memory: Arc, pub brain: Arc, pub scheduler: Arc, pub broadcaster: Arc, pub config: Arc, } 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 替代方案 如果确实需要感知能力,可通过以下方式实现: ```rust /// 前端感知事件(通过 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 Provider(Ollama 或 OpenAI) │ │ 7. 初始化 AgentBrain │ │ 8. 启动 TaskScheduler │ │ 9. 启动 WebSocket Server(用于实时推送) │ │ 10. 注册 Tauri Commands │ │ 11. 进入 Tauri 主循环 │ └─────────────────────────────────────────────────────────┘ ``` #### 3.7.2 代码结构 ```rust 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> { let state = app.state::>(); // 初始化记忆系统 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 配置 ```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 拖拽实现 ```tsx // 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 (
{/* Live2D Canvas */}
); }; ``` ### 4.2 Live2D Canvas 集成 #### 4.2.1 技术选型 使用 `live2d-cubism-core` + `@pixi/react-live2d` 或原生 Canvas 实现。 #### 4.2.2 集成方案 ```tsx // Live2DViewer.tsx import { useEffect, useRef } from 'react'; import * as LIVE2D from 'live2dcubismcore'; interface Live2DViewerProps { modelPath: string; animationName: string; emotionState: string; } const Live2DViewer: React.FC = ({ modelPath, animationName, emotionState }) => { const canvasRef = useRef(null); const modelRef = useRef(); 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 = { idle: 'idle', happy: 'happy', focused: 'focused', annoyed: 'annoyed', sleepy: 'sleepy', }; modelRef.current.internalModel.setExpression(expressionMap[emotionState]); } }, [emotionState]); return ( ); }; 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 连接管理 ```tsx // 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(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 事件处理 ```tsx // 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 (
); }; ``` ### 4.4 前端无业务逻辑原则 **核心原则:前端只负责渲染和转发,不处理任何业务逻辑。** | 前端职责 | 后端职责 | |----------|----------| | 显示 Live2D 动画 | 计算情绪状态 | | 响应用户拖拽 | 管理任务调度 | | 显示对话气泡 | 调用 LLM 决策 | | 转发用户输入到后端 | 存储记忆数据 | | 显示宠物状态 | 解析 Action 标签 | ```tsx // 正确示例:前端只做转发 const handleUserInput = async (message: string) => { const response = await invoke('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 // 前端 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 消息格式 ```json { "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 消息示例 ```json // 情绪变化 { "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 问题 1:main.py 调用不存在的 APIServer 类 **原问题描述:** > main.py调用 `api_mod.APIServer()` 但 server.py只有 `create_app()` 和 `run_server()`,无 APIServer 类 **处理方案:** 在 Rust 重构中,我们将: 1. 移除 `APIServer` 类的概念 2. 使用 Tauri Commands 作为 API 入口 3. `AppState` 单例直接在 Rust 层面实现,保证类型安全 ```rust // api.rs - 替代原 APIServer pub struct ApiLayer { state: Arc, } impl ApiLayer { pub fn new(state: Arc) -> 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 问题 2:DummyPetWindow 接口不一致 **原问题描述:** > `DummyPetWindow.isVisible` 是属性而 `PetWindow` 是方法 **处理方案:** 在 Rust 重构中,我们将: 1. 使用 Trait 定义统一的接口 2. 前端完全移除窗口相关的 Python 代码 3. Tauri 负责所有窗口操作 ```rust // 统一的窗口 Trait(Rust 后端不直接操作窗口,但保留接口定义) 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 命令控制窗口: ```typescript // 前端通过 invoke 调用窗口操作 await invoke('set_window_position', { x: 100, y: 200 }); await invoke('play_animation', { name: 'happy' }); ``` ### 11.3 问题 3:asyncio 嵌套调用可能导致状态隔离 **原问题描述:** > asyncio 嵌套调用可能导致状态隔离问题 **处理方案:** 在 Rust 重构中,我们将: 1. 使用单一 `tokio` runtime,避免嵌套 2. 所有异步操作通过同一个 runtime 执行 3. 状态通过 `Arc>` 共享 ```rust // 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 参考资源 - [Tauri 官方文档](https://tauri.app/) - [Tokio 异步运行时](https://tokio.rs/) - [Axum Web 框架](https://github.com/tokio-rs/axum) - [Live2D Cubism SDK](https://www.live2d.com/) - [SQLite Rust](https://github.com/rusqlite/rusqlite) --- **文档结束**