- 新增 docs/update/rust-refactor-design.md (1960行/66KB) - 涵盖11个章节:整体架构/模块设计/通信协议/进程模型/库选型等 - 设计目标:轻量化/极高能效/透明窗口/Live2D集成 - 基于原Python代码完整调研后的架构设计
65 KiB
65 KiB
EzVibe Rust 重构设计说明文档
文档版本: v1.0
编写日期: 2026-05-27
状态: 初稿
1. 设计目标与约束
1.1 核心设计目标
EzVibe 的 Rust 重构围绕以下四个核心目标展开:
| 目标 | 描述 | 量化指标 |
|---|---|---|
| 轻量化 | 保持宠物桌面的轻量特性,不引入沉重的运行时依赖 | 二进制体积 < 15MB(含 Live2D 资源) |
| 效率极高 | 极低的 CPU 占用,极快的响应时间 | 静息 CPU < 1%,TTFT < 50ms |
| 功耗极低 | 适合后台常驻,不影响系统续航 | 内存占用 < 80MB |
| 能效比极强 | 用最少的资源完成最多的工作 | 每小时交互成本最优 |
1.2 技术约束
- 透明窗口: 宠物需要显示在桌面上方,需要
transparent+ 无边框窗口 - Live2D 模型渲染: 前端需要原生支持 Canvas/WebGL,用于渲染 Live2D Cubism 模型
- 跨平台: 至少支持 Windows/macOS/Linux 三大平台
- LLM 后端兼容: 必须支持 Ollama(本地)和 OpenAI(云端)两套 LLM 接口
- 数据持久化: 记忆系统需要可靠的状态存储
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)进行通信。这种方式的优势:
- 零 IPC 开销: 无需跨进程通信,所有状态共享
- 简化部署: 单一二进制,更容易分发
- 原子操作: 避免多进程间的状态同步问题
- 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 模块
原因:
- Tauri 负责窗口操作: 用户明确表示 KeyboardMouseMonitor 不重构,Tauri 自己负责拖拽
- 窗口管理属于前端: 拖拽、点击检测等由 Tauri/前端处理更合适
- 简化后端职责: Rust 后端专注于 AI 逻辑,不涉及系统感知
- 性能考量: 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 Provider(Ollama 或 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:单一进程
理由:
- 性能优先: 宠物桌面是轻量应用,不需要进程隔离带来的安全性
- 简化状态管理: Rust 的所有权模型和 TypeScript 的强类型可以保证数据一致性
- 与 Tauri 架构匹配: Tauri 天然支持前端-后端紧耦合
- 降低复杂度: 减少跨进程通信的复杂度
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 风险点
- Live2D 集成: 前端 Live2D 模型加载和渲染可能遇到兼容性问题
- LLM Provider: Ollama/OpenAI API 的错误处理需要完善
- 状态同步: 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 重构中,我们将:
- 移除
APIServer类的概念 - 使用 Tauri Commands 作为 API 入口
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 问题 2:DummyPetWindow 接口不一致
原问题描述:
DummyPetWindow.isVisible是属性而PetWindow是方法
处理方案:
在 Rust 重构中,我们将:
- 使用 Trait 定义统一的接口
- 前端完全移除窗口相关的 Python 代码
- Tauri 负责所有窗口操作
// 统一的窗口 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 命令控制窗口:
// 前端通过 invoke 调用窗口操作
await invoke('set_window_position', { x: 100, y: 200 });
await invoke('play_animation', { name: 'happy' });
11.3 问题 3:asyncio 嵌套调用可能导致状态隔离
原问题描述:
asyncio 嵌套调用可能导致状态隔离问题
处理方案:
在 Rust 重构中,我们将:
- 使用单一
tokioruntime,避免嵌套 - 所有异步操作通过同一个 runtime 执行
- 状态通过
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 参考资源
文档结束