Files
EzVibeR/docs/impl/2026-06-14-session-summary.md
Claude Agent 5f830b3a1c Update: ReadMe
2026-06-14 16:40:02 +08:00

15 KiB
Raw Permalink Blame History

EzVibeR+ 2026-06-14 会话工作汇总

日期: 2026-06-14 分支: main 范围: 多会话持久化、提醒中心、截图存档、UI 调整、LLM 兼容性修复


一、整体工作概览

本次会话横跨 7 个独立但有交叉的功能模块,最终全部完成、编译通过、npm run tauri:dev 验证可运行。

# 模块 状态 用户原话要点
1 多会话聊天持久化JSONL + 图片) "每次聊天做记录 / 多会话 / 自动标题 / 搜索 / 导出"
2 提醒中心伪 session "reminder 单独占一个对话,叫提醒中心"
3 截图显示 + 发送给 AI 修复 "截图之后无法正常显示图片 / 发给AI不支持"
4 模型位置 + 浮动工具栏 "更靠近对话区域 / 调出右侧设置按钮"
5 LLM 兼容性detail / 超时) "invalid image detail / 60s 超时"
6 右侧滚动条修复 "滑一下整个界面全在动"
7 移除 help banner "把这段话删掉,每次都出现"

二、模块详解

2.1 多会话聊天持久化(最复杂的一坨)

用户原始诉求链

  1. "我需要你把图片持久地放在聊天区域内"(截图持久化)
  2. "对每次聊天做记录在日志文件中"JSONL 日志)
  3. "我列出来 4 个局限,改进所有"(自动标题 / 搜索 / 导出 / 内存不存 base64

架构决策:参考 /home/e2hang/.claude/plans/binary-puzzling-storm.mdplan mode 计划文件),用如下结构:

<app_data_dir>/sessions/
├── _index.json                # 轻量索引: [{id, title, msg_count, title_generated, ...}]
├── 2026-06-14_153500/         # session id = 创建时间戳
│   ├── chat.jsonl             # 一行一条 JSON 消息
│   └── images/<uuid>.{png,jpg} # 截图二进制

实现要点

文件 作用
src-tauri/src/modules/session.rs (新建 ~400 行) 索引读写 / jsonl 追加 / 截图存盘 / 搜索 / 导出
src-tauri/src/app/commands.rs 11 个 Tauri command: list_sessions, create_session, delete_session, rename_session, load_session_messages, append_message, save_chat_image, search_all_sessions, export_session, check_title_trigger, generate_session_title, ensure_reminder_session
src-tauri/src/main.rs invoke_handler! 注册所有新 command
src/live2d/hooks/useChat.ts (重写 ~600 行) 多会话状态、持久化防抖、搜索、导出、截图 listener
src/live2d/components/ChatPanel.vue (重写 ~600 行) 顶部 session 下拉 + 工具栏 + 全局搜索弹窗
src/live2d/components/MessageBubble.vue convertFileSrc 渲染截图 + <mark> 搜索高亮 + <think> 折叠

关键设计

  • 消息 ID 用 ${Date.now()}-${rand},避免重复
  • 持久化防抖 250ms (PERSIST_DEBOUNCE_MS):消息 push 后不立即写盘,连续 push 只写一次
  • 自动标题触发条件msg_count >= 5 && !title_generatedTITLE_TRIGGER_AT = 5),异步调 LLM 生成 ≤20 字短标题
  • 截图消息role: "screenshot",消息里只存 imagePath (JPEG) 和 imagePngPath (PNG)base64 不进内存

2.2 截图显示 + 发送给 AI 修复

Bug 现象

  • 截图后气泡里图片不显示(空白)
  • 点"发送给 AI"后报 60s 超时

问题诊断

Bug A — 图片不显示

  • MessageBubble.vue 用了 convertFileSrc(p)p 是相对路径 sessions/<id>/images/abc.jpg
  • Tauri 2 的 convertFileSrc 只接受绝对路径,相对路径会得到无效的 asset:// URL
  • 同时 tauri.conf.json 没开 assetProtocol.scopewebview 没权限访问 sessions/ 目录

Bug B — 发送给 AI 失败

  • useChat.sendImageInternal文件路径字符串当 base64 塞给 invoke("chat", { imageData })
  • Rust 把它当 base64 解码,喂给 LLM 全是乱码

解决思路

  1. 改 Rust chat 命令:从 image_data: Option<String> 改成 image_path: Option<String>,后端读文件、转 base64、构造 ImageAttachment
  2. load_chat_image 路径校验:拒绝绝对路径 / .. 路径遍历 / 非 png|jpg 后缀;限定路径必须在 sessions/<id>/images/ 下;文件大小上限 20MB
  3. tauri.conf.json 资产协议白名单scope: ["$APPDATA/sessions/**"]
  4. 改前端 MessageBubble:用 appDataDir() + join 拼绝对路径再 convertFileSrc,加 loading 占位

结果

  • src-tauri/src/app/commands.rs:220-274chat 命令签名
  • src-tauri/src/app/commands.rs:644-740 新增 load_chat_image 辅助函数
  • src-tauri/tauri.conf.json:33-40assetProtocol
  • src/live2d/components/MessageBubble.vue:4-65appDataDir + watch 异步刷新 src

2.3 提醒中心伪 session

用户原始决策"我希望 reminder 单独占一个对话,叫提醒中心"

架构设计

  • 保留 ID __reminders__,标题固定 🔔 提醒中心
  • 后端定时 tick 触发 reminder 时直接写到该 session 的 jsonl不依赖前端 listener
  • emit ezvibe:reminder 事件时附带 session_id 字段供前端切换
  • 前端 listener 收到后自动切到提醒中心视图
  • reminders session 不能删除 / 不能重命名 / 不参与 LLM 自动标题生成

实现要点

session.rs 新增:

pub const REMINDER_SESSION_ID: &str = "__reminders__";
pub const REMINDER_SESSION_TITLE: &str = "🔔 提醒中心";
pub fn is_reminder_session(id: &str) -> bool { ... }
pub fn ensure_reminder_session(app) -> Result<SessionMeta> { ... }
pub fn assert_not_reminder(id, op) -> Result<()> { ... }  // 拒绝 CRUD
pub fn append_reminder(app, action_type, message) -> Result<u32> { ... }

main.rs:78-127 改定时 tick 流程:

brain.think() → action
  → session::append_reminder(...)  // 直接落盘
  → emit("ezvibe:reminder", {..., session_id: REMINDER_SESSION_ID})
  → notify::send(...)               // OS 通知

useChat.ts listener 改动:

  • 收到 reminder → 切到 REMINDER_SESSION_IDload_session_messages reload
  • 不再 pushMessage(后端是权威源)

ChatPanel.vue UI 改动:

  • ✎ 重命名 / 🗑 删除按钮在 reminders session 变灰禁用
  • 新增 🔔 快速跳转按钮(高亮激活态)
  • isOnReminderCenter computed

2.4 模型位置 + 浮动工具栏

用户两轮反馈

  1. "让她更靠近对话区域"(模型偏右)
  2. "不不,我需要模型在那个模型界面的中间,但是增大模型的大小到身体宽度不超出模型界面范围,高度接近对话框"

最终公式loadModel + recenterModel 共享):

const leftPaneWidth = chatCollapsed ? w : (w - 340);
let modelHeight = h;              // 目标高度 = 窗口高(接近 chat 高度)
let modelWidth = modelHeight * 0.5; // 宽高比 0.5
if (modelWidth > leftPaneWidth) {   // 宽度超界 → 反推
  modelWidth = leftPaneWidth;
  modelHeight = modelWidth / 0.5;
}
model.x = (leftPaneWidth - modelWidth) / 2; // 水平居中
model.y = (h - modelHeight) / 2;            // 垂直居中

默认 600×480 窗口chat 开):左窗格 260×480 → 模型 240×480左右各 10px 余量,垂直贴满。

浮动工具栏修复

  • 问题.waifu-toolposition: absolute,但 .waifu 没设 position: relative,导致 absolute 落到 body被 chat 面板z-index 999盖住
  • 修复
    • .waifuposition: relative(变成定位上下文)
    • .waifu-tool 重写:竖排、半透明白底+模糊背景、紧贴左窗格右侧
    • z-index: 50(高于 hitbox=2低于 chat-pane=999
    • 默认可见,不依赖 hover
    • hover 高亮蓝色背景

9 个按钮都在(自上而下):💬 chat 切换 / ⚙ 配置 / ☐ 背景 / 👁 换模型 / 📍 位置 / ⬚ 大小 / ⓘ 复制URL / 🔒 穿透 / 关闭


2.5 LLM 兼容性detail / 超时)

两个错

错 A — invalid image detail: auto (2013)

  • 用户自部署的 OpenAI 兼容服务严格校验 detail 字段
  • OpenAI 官方 specdetail 是可选,默认 auto,但某些网关会拒绝
  • 解决:不传 detail 字段(openai.rs:148-156
  • 顺便Anthropic 用 type: "image" + source.base64,没这个问题

错 B — 请求超时60s: operation timed out

  • curl 测了 1.5s 就通401 是没带 key→ 网络/代理都正常
  • 慢在 LLM 视觉模型处理多模态(图片 base64 上传 + 视觉编码 + 文本生成)
  • 解决 1:超时 60s → 180sopenai.rs:45 / anthropic.rs:36
  • 解决 2:发送图片改用 JPEG 预览而不是 PNG 原图(MessageBubble.vue:17-26
    • PNG 几 MBbase64 后 7-20MB
    • JPEG 几十到几百 KB体积 ~1/10
    • LLM 不需要无损quality 80 的 JPEG 足够

2.6 右侧滚动条修复

现象"最右侧的那个滚动条,滑一下整个界面全在动"

根因

  • index.vue 里的 scoped CSS 写了 body, html { overflow: hidden }
  • Vue scoped CSS 会给选择器加 [data-v-xxxx] 属性选择器
  • body / html 标签上没有 data-v-xxxx 属性 → 这条规则被静默丢弃
  • 结果body 默认 overflow: visible → 透明窗口边缘出现滚动条,滑动带整体 layout 偏移

解决

  • live2d.html 顶部加 <style> 块直接锁定根容器
  • 顺便设了 background: transparent(桌面宠物透明窗口需要)
  • 删了 index.vue 里那段无效的 scoped 规则
<style>
  html, body { margin: 0; padding: 0; width: 100%; height: 100%;
               overflow: hidden; background: transparent; }
  #live2d-app { width: 100vw; height: 100vh; overflow: hidden; }
</style>

2.7 移除 help banner

用户反馈"现在把这段话删掉...每次都出现在窗口中"

根因onMountedif (!llmKeyConfigured) showHelp = true,每次启动 API key 未配置时强制展开帮助横幅。

解决:彻底移除 help banner 相关代码(不留任何痕迹):

  • const showHelp = ref(false) state
  • onMounted 里的强制展开
  • 顶栏 ? 切换按钮
  • .help-banner 整块 HTML包含 LLM 设置说明 + API key 警告 + Provider/Key/Model/URL 列表)
  • 对应的 scoped CSS

三、关键经验与通用原则

3.1 Tauri 2 的 convertFileSrc 必须是绝对路径

  • 相对路径(如 sessions/<id>/images/x.png)会得到无效的 asset:// URL图片无法加载
  • 必须先用 appDataDir() + join(...) 拼绝对路径
  • 同步的 appDataDir() 是 async前端要用 watch 监听 props 变化异步刷新 src

3.2 tauri.conf.jsonassetProtocol.scope 必须白名单放行

  • 默认 Tauri 2 不允许 webview 访问 file:// / asset:// 任意路径
  • 截图存在 app_data_dir 下,必须显式加 "$APPDATA/sessions/**" 到 scope
  • 不开 → convertFileSrc 拿到 URL 也读不出文件

3.3 前端 → 后端的 base64 数据要走命令参数,不走事件

  • 事件是单向 fire-and-forget传递大 base64 不合适
  • 让后端用路径作为 handle后端自己读文件转 base64
    • 避免 IPC 大 payload
    • 路径校验在后端做,安全性高
    • 前端只关心"哪个文件",不关心"内容"

3.4 Vue scoped CSS 对 body / html 无效

  • scoped CSS 的 [data-v-xxxx] 属性只加在组件根元素
  • 顶层标签(html / body / :root)永远不会有这个属性
  • 涉及全局的样式必须放 index.html / xxx.html 顶部的 <style> 块,或独立的全局 CSS 文件

3.5 Tailscale IP + clash-verge 代理冲突

  • .no_proxy() 是必须的clash-verge 会劫持 Tailscale IP100.64.0.0/10
  • 不加 → 内部服务走 clash 出去再绕回来,超时
  • 加 → 直连,正常
  • 之前我们修过这个 bug这次主要是 LLM 视觉模型本身慢

3.6 定时任务写入数据库要后端权威

  • 之前 reminder 走前端 listener前端 push → 触发持久化)→ 竞态currentSessionId 可能是 null
  • 改成后端直接 append_reminder 写 jsonl前端只负责 UI 切换
  • 单一数据源disk前端不再有"消息丢了 / 重复了"的问题

四、验证状态

检查项 结果
npx vue-tsc --noEmit EXIT 0
cargo check Finished仅 dead-code warnings
npm run tauri:dev 启动 成功,进程稳定运行
LLM 文本对话
截图保存到磁盘 ~/.local/share/com.ezviber.plus/sessions/<id>/images/
截图在气泡内显示 convertFileSrc + assetProtocol
截图发送给 AI load_chat_image + JPEG 优化)
多会话切换 下拉选
自动标题生成 5 条消息后异步调 LLM
当前会话搜索 <mark> 黄色高亮
全局搜索 弹窗 + 跨会话命中
导出 MD / JSON Blob 下载
reminder 写入提醒中心 60s tick / 手动 trigger
提醒中心视图自动切换
reminders session 删除/重命名保护 Rust 端拒绝
模型尺寸居中且接近 chat 高度 240×480 默认
浮动工具栏可见 右侧 9 个按钮
滚动条不出现 html/body locked
help banner 不再出现 彻底删除
LLM 60s 超时修复 180s + JPEG 优化

五、文件变更清单

后端 Rust新增 / 修改)

文件 类型 说明
src-tauri/src/modules/session.rs 新增 多会话、JSONL、截图存盘、搜索、导出、提醒中心
src-tauri/src/modules/screenshot.rs 沿用 xdg-desktop-portal 截屏(之前会话完成)
src-tauri/src/modules/notify.rs 沿用 系统通知封装(之前会话完成)
src-tauri/src/app/commands.rs 修改 +12 个 session/截图/搜索/导出 command, chat 改用 image_path
src-tauri/src/main.rs 修改 注册新 command, reminder 写入提醒中心
src-tauri/src/modules/mod.rs 修改 pub mod session;
src-tauri/tauri.conf.json 修改 assetProtocol.scope: ["$APPDATA/sessions/**"]
src-tauri/src/modules/openai.rs 修改 移除 detail:"auto", 超时 60→180s
src-tauri/src/modules/anthropic.rs 修改 超时 60→180s

前端(新增 / 修改)

文件 类型 说明
src/live2d/hooks/useChat.ts 重写 多会话状态、持久化、搜索、导出、提醒中心
src/live2d/components/ChatPanel.vue 重写 session 下拉 / 工具栏 / 全局搜索 / 提醒中心按钮
src/live2d/components/MessageBubble.vue 重写 截图气泡 / 搜索高亮 / 思考折叠 / 取消按钮
src/live2d/index.vue 修改 模型尺寸/位置公式, 浮动工具栏样式, 删冗余 body 规则
live2d.html 修改 顶部加 <style> 锁 body/html overflow

文档

文件 说明
docs/impl/2026-06-14-session-summary.md 本文档

六、未做 / 留给后续

备注
图片缩放(> 1024px 等比缩到 1024 长边) 用户没要,体积已经够小
reminders session 显示/隐藏开关 已在下拉第一行自然浮现,无需单独控制
截图历史清理(> 30 天自动删) 暂未做,磁盘不紧张
多模态 LLM 端点连通性测试 已知通;具体 LLM 处理速度是端点问题
reminders UI 角标(未读计数) 当前是直接切视图打断用户,简单粗暴;如需不打断可加角标