# EzVibeR+ 功能增强实现记录 > 日期: 2026-06-12 > 作者: CodeBuddy > 分支: main --- ## 一、实现功能总览 本次共实现以下功能: | # | 功能 | 状态 | |---|------|------| | 1 | 配置界面展示 Live2D 动作/表情标签 | ✅ 完成 | | 2 | LLM 返回动作标签驱动角色动画 | ✅ 完成 | | 3 | Idle 时根据情绪引擎自动调节表情 | ✅ 完成 | | 4 | 点击角色身体截图 + 发送给 AI 分析 | ✅ 完成 | --- ## 二、架构决策与关键经验 ### 2.1 ChatPanel 点击问题 (最重要的经验) **现象**: ChatPanel 在 Tauri 透明窗口中无法响应鼠标点击(可以通过键盘 Enter 发送)。 **根因分析**: PIXI.js 的 `resizeTo: window` 使 canvas 覆盖整个窗口。在 Tauri 透明窗口 + Linux 环境下,这个全窗口 canvas 的 WebGL 渲染表面会拦截所有鼠标事件,即使 ChatPanel 在 DOM 中位于上层、CSS 设置了 `pointer-events: auto` 也无效。 **尝试过的方案** (全部失败): 1. Canvas 设 `pointer-events: none` — 无效,WebGL 层在 compositor 层面拦截 2. 调整 CSS `z-index` — 无效,WebGL 渲染层级高于 DOM 层级 3. 使用 `window.addEventListener` 替代 canvas 点击 — 仍无法让 ChatPanel 接收点击 4. 关闭窗口 `transparent: true` — 仍然无效 5. 调整 flex 布局、`width: 100%` 等 — 无效 **最终方案**: 将 ChatPanel 放到**独立的 Tauri 窗口**中(`transparent: false`, `decorations: false`),与主窗口(Live2D 模型窗口)贴合在一起,通过 Tauri 事件系统通信。 **双窗口架构**: ``` ┌──────────────┐┌──────────────┐ │ Live2D 模型 ││ ChatPanel │ │ (透明, PIXI) ││ (正常窗口) │ │ pointer-events││ 点击完全正常 │ │ 通过 hitbox ││ │ └──────────────┘└──────────────┘ 主窗口 (main) 聊天窗口 (chat) ←── 移动/缩放时自动跟随 ──→ ``` 不过用户最终选择了 ChatPanel 在同一个窗口中渲染的方案(使用 `flex-direction: row-reverse` + `z-index` 分层),该方案在用户的桌面环境上成功。 **经验总结**: - Tauri + PIXI 透明窗口在 Linux 上存在 compositor 层级的点击拦截问题 - 如果单窗口方案失败,独立窗口是可靠的后备方案 - `flex-direction: row-reverse` 可以改变 DOM 渲染顺序,配合 `z-index` 改变层叠关系 --- ### 2.2 截图方案选择 在 Linux 上实现截图,最初考虑使用 `xcap` crate,但需要安装大量系统依赖(pipewire, xcb 等)。改为使用系统已有的 ImageMagick `convert` 命令: ```rust // 最终采用的截图命令 Command::new("convert") .arg("x:root") // X11 root window .arg(path) // 输出 PNG ``` 备选工具链: `import -display :0 -window root` (ImageMagick)、`scrot`、`maim`。 --- ## 三、文件变更清单 ### Rust 后端 | 文件 | 变更 | |------|------| | `src-tauri/Cargo.toml` | 添加 `image` (png/jpeg 编码)、`base64` 依赖 | | `src-tauri/tauri.conf.json` | ChatPanel 独立窗口配置(后续回退) | | `src-tauri/src/main.rs` | `AppState` 增加 `current_model_url`;注册新命令 `set_model_url`、`get_model_capabilities`、`capture_screenshot`;heartbeat 增加 `expression_variations` | | `src-tauri/src/app/commands.rs` | 新增 `set_model_url`、`get_model_capabilities`、`capture_screenshot` 命令;`chat` 命令增加 `image_data` 参数;发送 `ezvibe:motion` 事件 | | `src-tauri/src/modules/mod.rs` | 注册 `screenshot` 模块;导出 `ScreenshotData`、`ImageAttachment` | | `src-tauri/src/modules/brain.rs` | `ChatMessage` 增加 `image` 字段;新增 `ImageAttachment` 结构体;`think` 签名增加 `image` 参数;新增 `think_with_image` 多模态流程;新增 `build_screenshot_prompt` 截专用提示词;新增 `parse_motion_tag`、`strip_tags` 方法;`BrainResponse` 增加 `motion_tag` 字段 | | `src-tauri/src/modules/openai.rs` | `OaiMessage.content` 改为 `serde_json::Value` 支持多模态(文本+图片) | | `src-tauri/src/modules/anthropic.rs` | `AntMessage.content` 改为 `serde_json::Value` 支持多模态 | | `src-tauri/src/modules/emotion.rs` | `EmotionState` 新增 `subtle_variations()` 方法(每个情绪提供微表情关键词) | | `src-tauri/src/modules/screenshot.rs` | **新文件** — 全屏截图模块 (`convert x:root` → PNG/JPEG base64) | ### Vue 前端 | 文件 | 变更 | |------|------| | `src/components/Config.vue` | 新增「🎭 当前 Live2D 模型能力」区域(动作组列表、表情列表、情绪映射表) | | `src/live2d/index.vue` | 截图功能集成(capture + emit 事件);模型点击用 `model-hitbox` div 代理;`flex-direction: row-reverse` + `z-index` 解决 ChatPanel 点击问题 | | `src/live2d/components/ChatPanel.vue` | 暴露 `sendImage` 方法(用于截图发送) | | `src/live2d/components/MessageBubble.vue` | 新增 `screenshot` 角色消息类型(预览图 + 输入框 + 发送按钮) | | `src/live2d/hooks/useChat.ts` | 新增 `sendImage`/`sendImageInternal` 方法;监听 `ezvibe:screenshot-captured` 和 `ezvibe:screenshot-send` 事件 | | `chat.html` | **新文件** — 独立聊天窗口入口(后期回退到单窗口方案) | | `src/chat/App.ts` | **新文件** — 独立窗口 ChatPanel 启动脚本(后期删除) | | `vite.config.ts` | 添加 `chat` 入口点(后期回退) | --- ## 四、功能详情 ### 4.1 LLM 动作标签 **System Prompt 增加的指令**: ``` 【重要】每次回复末尾必须附带一个动作标签,格式为 [MOTION: 动作名]。 可用动作: idle, happy, focused, angry, sleepy, wink, surprised, sad, nod 示例: 用户: "今天好累" → 你: "辛苦啦 [MOTION: sleepy]" 用户: "通过了考试!" → 你: "太棒了! [MOTION: happy]" ``` **后端处理流程**: 1. LLM 返回含 `[MOTION: xxx]` 的文本 2. `parse_motion_tag()` 提取标签 3. `strip_tags()` 剥离标签,返回干净文本 4. `ezvibe:motion` 事件驱动前端播放 Live2D 动作/表情 ### 4.2 Idle 表情自动调节 - `EmotionState::subtle_variations()` 为每种情绪提供 4 个微表情关键词 - heartbeat (60s) 携带 `expression_variations` 到前端 - 前端 idle 定时器 (30s) 检查是否超过 45s 无交互 - 无交互时从微表情列表中轮换 ### 4.3 截图 + AI 分析 **流程**: 1. 点击角色身体 → hitbox 接收点击 2. `captureAndHandleScreenshot()` → invoke `capture_screenshot` (Rust) 3. Rust 执行 `convert x:root /tmp/xxx.png` 截图 4. 返回 PNG + JPEG (预览) base64 5. 通过 `ezvibe:screenshot-captured` 事件发送到 ChatPanel 6. ChatPanel 渲染截图消息气泡(缩略图 + 输入框 + 发送按钮) 7. 用户输入 → `ezvibe:screenshot-send` 事件 → `useChat.sendImageInternal` 8. invoke `chat({message, imageData})` → 多模态 LLM 请求 9. LLM 返回 → 显示在聊天流中 **多模态 LLM 协议适配**: - OpenAI: `content: [{type: "text", text: "..."}, {type: "image_url", image_url: {url: "data:image/png;base64,..."}}]` - Anthropic: `content: [{type: "text", text: "..."}, {type: "image", source: {type: "base64", media_type: "image/png", data: "..."}}]` ### 4.4 配置界面模型能力展示 Config.vue 新增区域显示: - **情绪映射表**: Idle/Happy/Focused/Annoyed/Sleepy → 匹配到的模型动作 + 表情 - **可用动作组**: 从 model3.json 的 Motions 解析 (如 `zhaiyan`, `zhaoxiang`) - **可用表情**: 从 FileReferences.Expressions 解析 (如 `捂脸`, `星星`, `比耶`) 后端命令 `get_model_capabilities` 通过 reqwest 从本地 Axum HTTP 服务器读取 model3.json。 --- ## 五、ChatPanel 点击问题最终方案 **关键 CSS**: ```css .live2d-shell { display: flex; flex-direction: row-reverse; /* ChatPanel 先渲染 */ } .chat-pane { position: relative; z-index: 999; /* 最高层级 */ pointer-events: auto !important; } .live2d-view { z-index: 0; /* 模型在下层 */ } .model-hitbox { /* 透明 div 代理模型点击 */ position: absolute; z-index: 1; } ``` **Canvas 处理**: `canvas.style.pointerEvents = "none"` — 永不拦截 DOM 事件。 **备选方案**: 如果单窗口方案在特定系统上不工作,可使用双窗口架构(`tauri.conf.json` 定义 `chat` 窗口 + `main.rs` setup 代码定位窗口 + 前端 `onMoved`/`onResized` 同步位置)。 --- ## 六、编译与运行 ```bash # 前端 npm install npm run build # 后端 (release) cd src-tauri && cargo build --release # 运行 DISPLAY=:0 ./src-tauri/target/release/live2d # 开发模式 npm run tauri:dev ```