- 新增 docs/update/rust-refactor-design.md (1960行/66KB) - 涵盖11个章节:整体架构/模块设计/通信协议/进程模型/库选型等 - 设计目标:轻量化/极高能效/透明窗口/Live2D集成 - 基于原Python代码完整调研后的架构设计
1961 lines
65 KiB
Markdown
1961 lines
65 KiB
Markdown
# 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<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 数据结构
|
||
|
||
```rust
|
||
/// 记忆条目
|
||
#[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 接口设计(策略模式)
|
||
|
||
```rust
|
||
/// 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 核心接口
|
||
|
||
```rust
|
||
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
|
||
|
||
```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::<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(策略模式)
|
||
|
||
```rust
|
||
/// 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 核心接口
|
||
|
||
```rust
|
||
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 标签解析
|
||
|
||
```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<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 核心接口
|
||
|
||
```rust
|
||
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 情绪调制函数
|
||
|
||
```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<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 事件推送
|
||
|
||
```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<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 单例
|
||
|
||
```rust
|
||
/// 应用全局状态(替代 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 替代方案
|
||
|
||
如果确实需要感知能力,可通过以下方式实现:
|
||
|
||
```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<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 配置
|
||
|
||
```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 (
|
||
<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 集成方案
|
||
|
||
```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<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 连接管理
|
||
|
||
```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<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 事件处理
|
||
|
||
```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 (
|
||
<div className="pet-window">
|
||
<Live2DViewer
|
||
emotionState={emotionState}
|
||
animationName={animationName}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
### 4.4 前端无业务逻辑原则
|
||
|
||
**核心原则:前端只负责渲染和转发,不处理任何业务逻辑。**
|
||
|
||
| 前端职责 | 后端职责 |
|
||
|----------|----------|
|
||
| 显示 Live2D 动画 | 计算情绪状态 |
|
||
| 响应用户拖拽 | 管理任务调度 |
|
||
| 显示对话气泡 | 调用 LLM 决策 |
|
||
| 转发用户输入到后端 | 存储记忆数据 |
|
||
| 显示宠物状态 | 解析 Action 标签 |
|
||
|
||
```tsx
|
||
// 正确示例:前端只做转发
|
||
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
|
||
// 前端 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<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 重构中,我们将:
|
||
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<RwLock<T>>` 共享
|
||
|
||
```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)
|
||
|
||
---
|
||
|
||
**文档结束** |