Files
EzVibeR/docs/impl/2026-06-12-feature-implementation.md
2026-06-14 16:29:59 +08:00

8.6 KiB
Raw Permalink Blame History

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

// 最终采用的截图命令
Command::new("convert")
    .arg("x:root")   // X11 root window
    .arg(path)       // 输出 PNG

备选工具链: import -display :0 -window root (ImageMagick)、scrotmaim


三、文件变更清单

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_urlget_model_capabilitiescapture_screenshotheartbeat 增加 expression_variations
src-tauri/src/app/commands.rs 新增 set_model_urlget_model_capabilitiescapture_screenshot 命令;chat 命令增加 image_data 参数;发送 ezvibe:motion 事件
src-tauri/src/modules/mod.rs 注册 screenshot 模块;导出 ScreenshotDataImageAttachment
src-tauri/src/modules/brain.rs ChatMessage 增加 image 字段;新增 ImageAttachment 结构体;think 签名增加 image 参数;新增 think_with_image 多模态流程;新增 build_screenshot_prompt 截专用提示词;新增 parse_motion_tagstrip_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-capturedezvibe: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:

.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