# EzVibeR+ 实现计划报告 ## 一、项目定位与融合策略 ### 1.1 融合方案:Option B - **技术栈基础**:`assets/tauri-live2d`(Vue3 + Tauri 1.x + PIXI.js + pixi-live2d-display) - **要移植的逻辑层**:EzVibeR 的 emotion/scheduler/memory/brain 四大模块 + api.rs 的 IPC 命令层 - **目标**:在 tauri-live2d 的双窗口 + 托盘 + Live2D 渲染架构上,叠加 EzVibeR 的事件驱动情感状态机 + 任务调度 + RAG 记忆 + LLM 分流大脑 ### 1.2 技术栈版本锁定 | 组件 | 版本 | 来源 | |------|------|------| | Tauri | 1.2.3 | tauri-live2d | | Vue | 3.x | tauri-live2d | | PIXI.js | 6.5.6 | tauri-live2d | | pixi-live2d-display | 0.4.0 | tauri-live2d | | Rust | 1.65+ | tauri-live2d | | tokio | 1.23 | tauri-live2d | | axum | 0.7.5 | tauri-live2d web_server | **新增 Rust 依赖(EzVibeR 逻辑层):** ```toml rusqlite = "0.29" # Memory SQLite 存储 ndarray = "0.15" # 向量索引 rand = "0.8" # Monte Carlo 采样(已有) bytemuck = "1.13" # f32↔u8 零拷贝转换 regex = "1" # Brain action 解析 tokio = { version = "1.23", features = ["macros", "sync", "time", "rt"] } # 扩展 features ``` --- ## 二、架构总览 ``` ┌─────────────────────────────────────────────────────────┐ │ Frontend (Vue3) │ │ ┌──────────────┐ ┌──────────────────────────┐ │ │ │ Config Window │ │ Live2D Window │ │ │ │ (index.html) │ 事件推送 │ (live2d.html) │ │ │ │ - 情感状态 │ ───────► │ - PIXI 渲染 Live2D 模型 │ │ │ │ - 记忆搜索 │ │ - 碰撞检测 / 拖拽区域 │ │ │ │ - LLM 对话 │ │ - 表情动画驱动 │ │ │ └──────────────┘ └──────────────────────────┘ │ └────────────────────────┬────────────────────────────────┘ │ Tauri invoke / emit ┌────────────────────────▼────────────────────────────────┐ │ Backend (Rust) │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ │ EmotionEngine │ │ TaskScheduler │ │ AgentBrain │ │ │ │ (emotion.rs) │ │ (scheduler.rs)│ │ (brain.rs) │ │ │ │ - 5 状态机 │ │ - 60s tick │ │ - LLM 分流 │ │ │ │ - MonteCarlo │ │ - 行为触发 │ │ - FallbackCache │ │ │ │ - 事件驱动 │ │ - 静默小时 │ │ - RAG 注入 │ │ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │ ┌─────────────────────────────────────────────────────┐│ │ │ MemorySystem (memory.rs) ││ │ │ SQLite(WAL) + ndarray 向量索引 + 500ms 重建防抖 ││ │ └─────────────────────────────────────────────────────┘│ │ ┌──────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │ │ commands.rs │ │ menu.rs │ │ web_server │ │ │ │ - IPC 命令 │ │ - 托盘菜单 │ │ - 模型文件服务 │ │ │ └──────────────┘ └─────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` --- ## 三、Tauri-live2d 既有实现分析 ### 3.1 双窗口模型 - **Live2D 窗口** (`main`): 215×200, transparent, frameless, alwaysOnTop, skipTaskbar - **Config 窗口** (`config`): 有标题栏,可调整大小,alwaysOnTop - 窗口按需创建/重建:`on_system_tray_event` 中处理 `show` / `config` 点击 ### 3.2 系统托盘 - 托盘图标:`icons/icon.png` - 菜单项:显示桌宠 / 隐藏桌宠 / 分隔线 / 配置中心 / 分隔线 / 关闭软件 - 窗口重建模式:窗口被关闭后,点击托盘菜单会重新 `WindowBuilder::new` ### 3.3 插件机制 - **autostart** 插件:使用 `auto-launch` crate,支持 macOS LaunchAgent / Windows / Linux - **checkupdate** 插件:调用 `app.updater().check()`,弹窗提示下载安装 ### 3.4 web_server - Axum HTTP 服务器,随机端口(1024-65535) - CORS 全开,服务本地模型目录文件 - WebSocket `/ws` 端点(当前为 echo) --- ## 四、待移植的 EzVibeR 底层逻辑 ### 4.1 EmotionEngine(emotion.rs) **核心设计:** - 5 状态:`Idle` / `Happy` / `Focused` / `Annoyed` / `Sleepy` - 8 事件:`UserInteract` / `UserPraise` / `UserFocused` / `ReminderIgnored` / `SedentaryTooLong` / `LongWorkSession` / `TimePasses` / `UserHealthyAction` - 转换逻辑:事件 → boost 增益向量 → 与转移矩阵叠加 → softmax → Monte Carlo 采样 - 5 秒最低驻留时间防止抖动 **关键数据结构:** ```rust pub enum EmotionState { Idle, Happy, Focused, Annoyed, Sleepy } pub enum EventType { ... } // 每个事件返回 3 个 (target, gain) boost pub trait Event { fn boosts(&self) -> Vec<(EmotionState, f32)>; } // EmotionEngine 内部状态 struct EmotionEngine { state: EmotionState, transition_matrix: [[f32; 5]; 5], // 基础转移概率 state_since: Instant, // 进入当前状态的时间 min_residence: Duration, // 最低驻留 rng: SmallRng, // 随机数 } ``` ### 4.2 TaskScheduler(scheduler.rs) **核心设计:** - 行为列表 (`Behavior`): name / action_type / interval_secs / quiet_hours / priority - 内置 P0 行为:`remind_water` (45min) / `remind_stretch` (60min) - `on_timer_tick()` 由 Rust 后端的 60s 背景循环调用 - 静默小时:22:00-08:00 自动抑制提醒 ### 4.3 MemorySystem(memory.rs) **核心设计(三层):** ``` 写入路径: 用户输入 → SQLite(WAL) 立即持久化 → pending_queue → 500ms 防抖 → 合并到 ndarray 向量索引 查询路径: 查询 → embed() → VectorIndex.search(top-K) → SQLite 查完整文本 → 返回 SearchResult ``` **关键数据结构:** ```rust struct MemoryConfig { db_path, rag_top_k, min_similarity, rebuild_threshold } struct MemoryEntry { id, text, embedding: Vec, tags, metadata, created_at } struct SearchResult { id, text, similarity, tags, created_at } trait Embedder { fn embed(&self, text: &str) -> Vec; fn dimension(&self) -> usize; } struct DummyEmbedder { ... } // 基于 hash 的伪嵌入(384维) struct SqliteStore { ... } // WAL + bytemuck::cast_slice 零拷贝 BLOB struct VectorIndex { array: Array2, l2_norm, ids: Vec } struct MemorySystem { store, index, embedder, pending_queue, rebuild_tx } ``` ### 4.4 AgentBrain(brain.rs) **核心设计(能量分流):** | InputType | 处理路径 | LLM 调用 | |-----------|---------|---------| | `UserMessage` | 完整 RAG → LLM → 记忆写入 | **是** | | `SystemReminder` | FallbackCache 查表(离线消息) | 否 | | `Heartbeat` | 直接返回 `BrainResponse::idle()` | 否 | **FallbackCache 内置消息(EzVibeR 原有):** - `remind_water`: 5 条随机消息 - `remind_stretch`: 5 条随机消息 - `remind_eyes`: 5 条随机消息 - `greet_morning/afternoon/evening`: 各 3 条 **Action 解析:** ```rust // 来自 LLM 的响应中解析 [ACTION: type: message] lazy_regex!(ACTION_RE, r"\[ACTION:\s*(\w+):\s*([^\]]+)\]"); ``` --- ## 五、实施步骤详解 ### 步骤 1:项目骨架搭建 ``` EzVibeR+/ ├── docs/ │ └── impl/ │ └── report.md ← 本文档 ├── src/ ← Vue3 前端(来自 tauri-live2d) │ ├── main.ts │ ├── App.vue │ ├── live2d/ │ │ ├── App.ts │ │ └── index.vue │ ├── components/ │ │ ├── Config.vue │ │ └── Model.vue │ ├── plugins/ ← Tauri plugin JS wrapper │ ├── hooks/ ← Vue composition utilities │ └── util/ ├── src-tauri/ ← Rust 后端 │ ├── Cargo.toml ← 新增 rusqlite/ndarray/bytemuck/regex │ ├── tauri.conf.json ← 来自 tauri-live2d │ ├── src/ │ │ ├── main.rs ← 来自 tauri-live2d(稍作修改) │ │ ├── app/ ← 来自 tauri-live2d │ │ ├── modules/ ← 从 EzVibeR 移植 │ │ │ ├── emotion.rs │ │ │ ├── scheduler.rs │ │ │ ├── memory.rs │ │ │ └── brain.rs │ │ └── plugins/ ← 来自 tauri-live2d │ └── web_server/ ← 来自 tauri-live2d ``` **操作:** 1. 复制 `assets/tauri-live2d/` 的前端 `src/`、后端 `src-tauri/` 到 `EzVibeR+/` 2. 保留 `tauri-live2d` 的 `main.rs`(仅修改以适配新模块) 3. 保留 `tauri-live2d` 的 `menu.rs` / `commands.rs`(托盘和 IPC 基础) ### 步骤 2:前端改造 **文件修改清单:** 1. **`src/hooks/useBackendEvents.ts`**(新建) - 复用 EzVibeR 的事件 hook 模式 - 监听 `ezvibe:emotion` / `ezvibe:reminder` / `ezvibe:heartbeat` / `ezvibe:action` - 返回状态到 Vue 组件 2. **`src/hooks/useLookAt.ts`**(新建) - 从 EzVibeR 移植(鼠标 EMA 追踪) 3. **`src/components/Config.vue`**(改造) - 复用 tauri-live2d 的 Config.vue 结构 - 添加 EzVibeR 状态展示区(当前情绪、记忆条数、调度状态) - 添加 LLM 对话 UI(chat 输入框 + 响应展示) - 添加记忆搜索 UI - 暴露 `invoke("chat")` / `invoke("interact")` / `invoke("search_memories")` 4. **`src/live2d/index.vue`**(改造) - 复用 tauri-live2d 的 Live2D 渲染 - 从 `useBackendEvents` 获取情绪状态,驱动 PIXI 动画 5. **`src/plugins/index.ts`**(保留 tauri-live2d 原样) ### 步骤 3:Rust 模块移植(核心任务) #### 3.1 emotion.rs 移植 **从 EzVibeR 复制并做以下适配:** 1. 路径调整:从 `crate::modules::emotion` → 保持不变(EzVibeR 路径结构) 2. 移除 Tauri 绑定:移除 `serde`、`SmallRng`、`Instant` 以外的任何 Tauri 依赖 3. 事件类型保持不变 **新增接口(供 commands.rs 调用):** ```rust pub fn new_with_config() -> EmotionEngine pub fn on_event(&mut self, event: EventType) -> EmotionState pub fn get_state(&self) -> EmotionState pub fn get_state_since(&self) -> Instant ``` #### 3.2 scheduler.rs 移植 **从 EzVibeR 复制并适配:** 1. `Behavior` 结构保持不变(name/action_type/interval_secs/quiet_hours 等) 2. `TaskScheduler::on_timer_tick()` 签名保持:`fn on_timer_tick(&mut self, emotion: &mut EmotionEngine) -> Vec` 3. 内置行为:`remind_water` / `remind_stretch` 保持不变 **新增接口:** ```rust pub fn new_with_config() -> TaskScheduler pub fn on_timer_tick(&mut self, emotion: &mut EmotionEngine) -> Vec pub fn get_status(&self) -> Vec ``` #### 3.3 memory.rs 移植 **从 EzVibeR 复制并适配:** 1. `Embedder` trait + `DummyEmbedder` 保持不变 2. `SqliteStore` / `VectorIndex` / `MemorySystem` 保持不变 3. 配置路径:数据库放在 `app_data_dir()/ezvibe/memory.db` 4. 重建防抖(500ms tokio timeout)保持不变 **新增接口:** ```rust pub fn new_with_config(app_data_dir: PathBuf) -> MemorySystem pub fn add(&mut self, text: &str, tags: Vec<&str>) -> Result pub fn search(&self, query: &str, top_k: usize) -> Result> pub fn count(&self) -> usize ``` #### 3.4 brain.rs 移植 **从 EzVibeR 复制并适配:** 1. `InputType` / `LLMProvider` trait / `AgentBrain` 保持不变 2. `FallbackCache` 内置消息保持不变 3. `Action` 解析正则保持:`ACTION_RE` 4. LLM 接口:需要用户配置 API key(存入 AppConf) **新增接口:** ```rust pub fn new(memory: Arc) -> AgentBrain pub fn think(&self, input_type: InputType, text: &str) -> BrainResponse ``` ### 步骤 4:api.rs 改造(Tauri IPC 命令层) **来自 tauri-live2d 的 commands.rs 基础上,添加 EzVibeR 的命令:** ```rust // 原有(来自 tauri-live2d) read_file / write_file / model_list / read_config / write_config // 新增(来自 EzVibeR api.rs) get_emotion() // → EmotionResponse get_scheduler_status() // → Vec get_memory_count() // → usize search_memories(query: String) // → Vec interact(event: String) // → InteractResponse chat(message: String) // → BrainResponse trigger_reminder(action_type: String) // → BrainResponse ``` ### 步骤 5:lib.rs / main.rs 改造 **基于 tauri-live2d 的 main.rs,添加:** 1. 引入新模块:`mod modules { emotion, scheduler, memory, brain }` 2. 创建 `AppState`(类似 EzVibeR 的 `Arc>` 等) 3. 替换 `manage(port)` → `manage(app_state)` 4. **60s 背景循环**(从 EzVibeR lib.rs 移植): ```rust // Phase 1: scheduler tick → inject TimePasses → check behaviors // Phase 2: for each fired behavior → brain.think(SystemReminder) → emit ezvibe:reminder // Phase 3: emit ezvibe:heartbeat ``` 5. 事件推送(emit):使用 tauri-live2d 的 `app.emit()` 方式 ### 步骤 6:Config 扩展 **扩展 tauri-live2d 的 AppConf(config.rs):** ```rust pub struct AppConf { // 来自 tauri-live2d(保留) pub port: u16, pub model_dir: String, pub width: u16, pub height: u16, pub x: u16, pub y: u16, pub check_update: bool, pub remote_list: Vec, pub model_block: bool, pub auto_start: bool, // 来自 EzVibeR(新增) pub llm_api_key: String, // LLM API 密钥 pub llm_base_url: String, // LLM API Base URL pub memory_enabled: bool, // 记忆系统开关 } ``` ### 步骤 7:前端事件对接 **`src/hooks/useBackendEvents.ts` 事件列表:** | 事件名 | payload | 来源 | |--------|---------|------| | `ezvibe:emotion` | `{state, state_since}` | Rust emit | | `ezvibe:reminder` | `{action_type, message, priority}` | Rust emit | | `ezvibe:heartbeat` | `{tick, uptime, emotion_state}` | Rust emit | | `ezvibe:action` | `{action_type, message, priority}` | Rust emit | **`Config.vue` 组件中消费这些事件:** - 情感状态面板实时更新 - 提醒弹窗(toast) - 心跳指示器 --- ## 六、关键实现细节 ### 6.1 情感状态 ↔ Live2D 动画联动 当 `ezvibe:emotion` 事件触发时,`Live2DWindow` 组件根据情绪状态切换 PIXI 动画: ```javascript // src/live2d/index.vue watch(() => emotionState, (state) => { if (state === 'Happy') { model.motion("happy") } else if (state === 'Annoyed') { model.motion("annoyed") } }) ``` ### 6.2 记忆搜索 RAG 流程 ``` User 输入 → Config.vue invoke("chat", {message}) → Rust brain.think(UserMessage, text) → memory.search(text, top_k=3) → 构建 context → LLM.chat(context + history) → 返回 BrainResponse → emit("ezvibe:action") → Config.vue 显示 LLM 响应 ``` ### 6.3 托盘菜单事件 → 情绪触发 ``` 托盘点击"显示桌宠" → window.show() → 可选:inject UserInteract 事件 托盘点击"隐藏桌宠" → window.hide() ``` ### 6.4 静默小时行为抑制 ```rust // scheduler.rs if behavior.quiet_hours_enabled { let hour = chrono::Local::now().hour(); if (hour >= 22) || (hour < 8) { continue; } // 静默时段跳过 } ``` --- ## 七、文件操作清单 ### 前端(Vue3) | 操作 | 文件 | |------|------| | 复制(来自 tauri-live2d) | `src/main.ts`, `src/App.vue`, `src/style.css` | | 复制(来自 tauri-live2d) | `src/live2d/App.ts`, `src/live2d/index.vue` | | 复制(来自 tauri-live2d) | `src/components/Config.vue`, `src/components/Model.vue` | | 复制(来自 tauri-live2d) | `src/plugins/*.ts`, `src/hooks/*.ts` | | 复制(来自 tauri-live2d) | `src/util/*.ts`, `src/types/*.d.ts` | | 新建 | `src/hooks/useBackendEvents.ts` | | 新建 | `src/hooks/useLookAt.ts` | | 改造 | `src/components/Config.vue`(集成 EzVibeR UI) | ### Rust(src-tauri) | 操作 | 文件 | |------|------| | 复制(来自 tauri-live2d) | `src-tauri/Cargo.toml`, `src-tauri/build.rs` | | 改造(基于 tauri-live2d main.rs) | `src-tauri/src/main.rs` | | 复制(来自 tauri-live2d) | `src-tauri/src/app/config.rs` | | 改造(基于 tauri-live2d menu.rs) | `src-tauri/src/app/menu.rs` | | 改造(基于 tauri-live2d commands.rs) | `src-tauri/src/app/commands.rs` | | 复制(来自 tauri-live2d) | `src-tauri/src/app/mstruct.rs` | | 移植(来自 EzVibeR) | `src-tauri/src/modules/emotion.rs` | | 移植(来自 EzVibeR) | `src-tauri/src/modules/scheduler.rs` | | 移植(来自 EzVibeR) | `src-tauri/src/modules/memory.rs` | | 移植(来自 EzVibeR) | `src-tauri/src/modules/brain.rs` | | 改造(基于 tauri-live2d plugins) | `src-tauri/src/plugins/autostart.rs` | | 改造(基于 tauri-live2d plugins) | `src-tauri/src/plugins/checkupdate.rs` | | 复制(来自 tauri-live2d) | `src-tauri/web_server/` | --- ## 八、审查要点 1. **技术栈确认**:是否严格基于 tauri-live2d(Tauri 1.x,Vue3)而非 EzVibeR(Tauri 2.x,React)? 2. **模块边界**:emotion/scheduler/memory/brain 是否完整从 EzVibeR 移植,无遗漏核心逻辑? 3. **API 兼容性**:前端 `invoke()` 调用格式是否与后端 `#[tauri::command]` 匹配? 4. **事件通道**:`emit` 的事件名是否与前端 `listen` 完全一致? 5. **Config 扩展**:新增字段(llm_api_key 等)是否向后兼容原有字段? 6. **风险项**:LLM 接口(GPT/MiniMax)的 embed 函数是否需要实现真实调用? --- **状态:待审核**