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

19 KiB
Raw Permalink Blame History

EzVibeR+ 实现计划报告

一、项目定位与融合策略

1.1 融合方案Option B

  • 技术栈基础assets/tauri-live2dVue3 + 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-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 秒最低驻留时间防止抖动

关键数据结构:

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

关键数据结构:

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 解析:

// 来自 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-live2dmain.rs(仅修改以适配新模块)
  3. 保留 tauri-live2dmenu.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 绑定:移除 serdeSmallRngInstant 以外的任何 Tauri 依赖
  3. 事件类型保持不变

新增接口(供 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 复制并适配:

  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 保持不变

新增接口:

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保持不变

新增接口:

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

新增接口:

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 的命令:

// 原有(来自 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 移植):
    // 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

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

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 函数是否需要实现真实调用?

状态:待审核