- 修复配置窗口'选择本地模型'按钮无响应问题 - 添加 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>
19 KiB
19 KiB
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 逻辑层):
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-launchcrate,支持 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 秒最低驻留时间防止抖动
关键数据结构:
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
关键数据结构:
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 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 解析:
// 来自 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
操作:
- 复制
assets/tauri-live2d/的前端src/、后端src-tauri/到EzVibeR+/ - 保留
tauri-live2d的main.rs(仅修改以适配新模块) - 保留
tauri-live2d的menu.rs/commands.rs(托盘和 IPC 基础)
步骤 2:前端改造
文件修改清单:
-
src/hooks/useBackendEvents.ts(新建)- 复用 EzVibeR 的事件 hook 模式
- 监听
ezvibe:emotion/ezvibe:reminder/ezvibe:heartbeat/ezvibe:action - 返回状态到 Vue 组件
-
src/hooks/useLookAt.ts(新建)- 从 EzVibeR 移植(鼠标 EMA 追踪)
-
src/components/Config.vue(改造)- 复用 tauri-live2d 的 Config.vue 结构
- 添加 EzVibeR 状态展示区(当前情绪、记忆条数、调度状态)
- 添加 LLM 对话 UI(chat 输入框 + 响应展示)
- 添加记忆搜索 UI
- 暴露
invoke("chat")/invoke("interact")/invoke("search_memories")
-
src/live2d/index.vue(改造)- 复用 tauri-live2d 的 Live2D 渲染
- 从
useBackendEvents获取情绪状态,驱动 PIXI 动画
-
src/plugins/index.ts(保留 tauri-live2d 原样)
步骤 3:Rust 模块移植(核心任务)
3.1 emotion.rs 移植
从 EzVibeR 复制并做以下适配:
- 路径调整:从
crate::modules::emotion→ 保持不变(EzVibeR 路径结构) - 移除 Tauri 绑定:移除
serde、SmallRng、Instant以外的任何 Tauri 依赖 - 事件类型保持不变
新增接口(供 commands.rs 调用):
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 复制并适配:
Behavior结构保持不变(name/action_type/interval_secs/quiet_hours 等)TaskScheduler::on_timer_tick()签名保持:fn on_timer_tick(&mut self, emotion: &mut EmotionEngine) -> Vec<Behavior>- 内置行为:
remind_water/remind_stretch保持不变
新增接口:
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 复制并适配:
Embeddertrait +DummyEmbedder保持不变SqliteStore/VectorIndex/MemorySystem保持不变- 配置路径:数据库放在
app_data_dir()/ezvibe/memory.db - 重建防抖(500ms tokio timeout)保持不变
新增接口:
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 复制并适配:
InputType/LLMProvidertrait /AgentBrain保持不变FallbackCache内置消息保持不变Action解析正则保持:ACTION_RE- LLM 接口:需要用户配置 API key(存入 AppConf)
新增接口:
pub fn new(memory: Arc<MemorySystem>) -> AgentBrain
pub fn think(&self, input_type: InputType, text: &str) -> BrainResponse
步骤 4:api.rs 改造(Tauri IPC 命令层)
来自 tauri-live2d 的 commands.rs 基础上,添加 EzVibeR 的命令:
// 原有(来自 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
步骤 5:lib.rs / main.rs 改造
基于 tauri-live2d 的 main.rs,添加:
- 引入新模块:
mod modules { emotion, scheduler, memory, brain } - 创建
AppState(类似 EzVibeR 的Arc<Mutex<EmotionEngine>>等) - 替换
manage(port)→manage(app_state) - 60s 背景循环(从 EzVibeR lib.rs 移植):
// 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 - 事件推送(emit):使用 tauri-live2d 的
app.emit()方式
步骤 6:Config 扩展
扩展 tauri-live2d 的 AppConf(config.rs):
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 动画:
// 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 静默小时行为抑制
// 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/ |
八、审查要点
- 技术栈确认:是否严格基于 tauri-live2d(Tauri 1.x,Vue3)而非 EzVibeR(Tauri 2.x,React)?
- 模块边界:emotion/scheduler/memory/brain 是否完整从 EzVibeR 移植,无遗漏核心逻辑?
- API 兼容性:前端
invoke()调用格式是否与后端#[tauri::command]匹配? - 事件通道:
emit的事件名是否与前端listen完全一致? - Config 扩展:新增字段(llm_api_key 等)是否向后兼容原有字段?
- 风险项:LLM 接口(GPT/MiniMax)的 embed 函数是否需要实现真实调用?
状态:待审核