Files
EzVibeR/docs/impl/report.md
Claude Agent 51ddaee7d0 feat: 修复多个问题并更新文档
- 修复配置窗口'选择本地模型'按钮无响应问题
  - 添加 tauri-plugin-dialog 和 tauri-plugin-clipboard-manager 依赖
  - 在 main.rs 中注册插件
  - 创建 capabilities/default.json 配置权限

- 修复工具栏按钮不显示问题
  - 将 .waifu-tool 的 display 从 none 改为 block

- 修复模型显示比例问题
  - 禁用 reloadPositionScale 避免覆盖尺寸设置
  - 移除 onResized 回调中的模型尺寸重置
  - 设置模型宽度为窗口的 50%

- 修复切换 workspace 后模型尺寸恢复问题

- 添加窗口置顶设置,显示时重新设置 always_on_top

- 更新 CLAUDE.md 文档
- 添加 .gitignore
- 更新 README.md
- 添加 docs/impl/debug-log-20260531.md 记录调试过程

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:42:56 +08:00

489 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 EmotionEngineemotion.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 TaskSchedulerscheduler.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 MemorySystemmemory.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<f32>, tags, metadata, created_at }
struct SearchResult { id, text, similarity, tags, created_at }
trait Embedder { fn embed(&self, text: &str) -> Vec<f32>; fn dimension(&self) -> usize; }
struct DummyEmbedder { ... } // 基于 hash 的伪嵌入384维
struct SqliteStore { ... } // WAL + bytemuck::cast_slice 零拷贝 BLOB
struct VectorIndex { array: Array2<f32>, l2_norm, ids: Vec<i64> }
struct MemorySystem { store, index, embedder, pending_queue, rebuild_tx }
```
### 4.4 AgentBrainbrain.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 对话 UIchat 输入框 + 响应展示)
- 添加记忆搜索 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 原样)
### 步骤 3Rust 模块移植(核心任务)
#### 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<Behavior>`
3. 内置行为:`remind_water` / `remind_stretch` 保持不变
**新增接口:**
```rust
pub fn new_with_config() -> TaskScheduler
pub fn on_timer_tick(&mut self, emotion: &mut EmotionEngine) -> Vec<Behavior>
pub fn get_status(&self) -> Vec<BehaviorStatus>
```
#### 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<i64>
pub fn search(&self, query: &str, top_k: usize) -> Result<Vec<SearchResult>>
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<MemorySystem>) -> AgentBrain
pub fn think(&self, input_type: InputType, text: &str) -> BrainResponse
```
### 步骤 4api.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<BehaviorStatus>
get_memory_count() // → usize
search_memories(query: String) // → Vec<SearchResult>
interact(event: String) // → InteractResponse
chat(message: String) // → BrainResponse
trigger_reminder(action_type: String) // → BrainResponse
```
### 步骤 5lib.rs / main.rs 改造
**基于 tauri-live2d 的 main.rs添加**
1. 引入新模块:`mod modules { emotion, scheduler, memory, brain }`
2. 创建 `AppState`(类似 EzVibeR 的 `Arc<Mutex<EmotionEngine>>` 等)
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()` 方式
### 步骤 6Config 扩展
**扩展 tauri-live2d 的 AppConfconfig.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<String>,
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 |
### Rustsrc-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-live2dTauri 1.xVue3而非 EzVibeRTauri 2.xReact
2. **模块边界**emotion/scheduler/memory/brain 是否完整从 EzVibeR 移植,无遗漏核心逻辑?
3. **API 兼容性**:前端 `invoke()` 调用格式是否与后端 `#[tauri::command]` 匹配?
4. **事件通道**`emit` 的事件名是否与前端 `listen` 完全一致?
5. **Config 扩展**新增字段llm_api_key 等)是否向后兼容原有字段?
6. **风险项**LLM 接口GPT/MiniMax的 embed 函数是否需要实现真实调用?
---
**状态:待审核**