8.6 KiB
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 也无效。
尝试过的方案 (全部失败):
- Canvas 设
pointer-events: none— 无效,WebGL 层在 compositor 层面拦截 - 调整 CSS
z-index— 无效,WebGL 渲染层级高于 DOM 层级 - 使用
window.addEventListener替代 canvas 点击 — 仍无法让 ChatPanel 接收点击 - 关闭窗口
transparent: true— 仍然无效 - 调整 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 命令:
// 最终采用的截图命令
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]"
后端处理流程:
- LLM 返回含
[MOTION: xxx]的文本 parse_motion_tag()提取标签strip_tags()剥离标签,返回干净文本ezvibe:motion事件驱动前端播放 Live2D 动作/表情
4.2 Idle 表情自动调节
EmotionState::subtle_variations()为每种情绪提供 4 个微表情关键词- heartbeat (60s) 携带
expression_variations到前端 - 前端 idle 定时器 (30s) 检查是否超过 45s 无交互
- 无交互时从微表情列表中轮换
4.3 截图 + AI 分析
流程:
- 点击角色身体 → hitbox 接收点击
captureAndHandleScreenshot()→ invokecapture_screenshot(Rust)- Rust 执行
convert x:root /tmp/xxx.png截图 - 返回 PNG + JPEG (预览) base64
- 通过
ezvibe:screenshot-captured事件发送到 ChatPanel - ChatPanel 渲染截图消息气泡(缩略图 + 输入框 + 发送按钮)
- 用户输入 →
ezvibe:screenshot-send事件 →useChat.sendImageInternal - invoke
chat({message, imageData})→ 多模态 LLM 请求 - 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:
.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 同步位置)。
六、编译与运行
# 前端
npm install
npm run build
# 后端 (release)
cd src-tauri && cargo build --release
# 运行
DISPLAY=:0 ./src-tauri/target/release/live2d
# 开发模式
npm run tauri:dev