# 05 · 聊天功能
聊天面板位于桌宠窗口**右侧**(默认 340 px 宽)。可一键收起让模型占满窗口。
> 第一次启动时聊天面板**默认展开**。改 `chatCollapsed` 状态:在工具栏点 **💬 按钮**(顶部第一个)即可。
## 1. 面板顶部一条
```
[●] [会话下拉(标题 消息数)] [+新建] [✎重命名] [🗑删除] ___ [🔔提醒中心] [🔍搜索导出]
```
| 元素 | 行为 |
|---|---|
| **● 状态点** | 🟢就绪 / 🟠思考中 / 🔴API key 未配置 / ⚪未知 |
| **会话下拉** | 切换已有会话,按 `updated_at` 倒序 |
| **+** | 新建一个空会话(标题默认 `对话 MM-DD HH:MM`) |
| **✎** | 重命名当前会话(标题 ≤ 40 字符) |
| **🗑** | 删除当前会话(确认弹窗) |
| **🔔** | 跳到「提醒中心」伪会话(激活态高亮) |
| **🔍** | 展开/收起第二行工具栏(搜索 + 导出) |
> 在「提醒中心」时,✎ 和 🗑 **自动禁用**(提醒中心不可重命名/删除)。
## 2. 第二行(点击 🔍 展开)
| 元素 | 行为 |
|---|---|
| **搜索框** | 输入关键字回车 → 当前会话内即时过滤 + 黄色高亮 |
| **🌐 全部** | 弹出全局搜索弹窗(跨会话),点命中条目跳过去 |
| **⬇ MD** | 把当前会话导出为 Markdown 文件,浏览器自动下载 |
| **⬇ JSON** | 把当前会话导出为 JSON 文件,浏览器自动下载 |
## 3. 消息流
按**时间顺序**展示当前会话的所有消息。跨日时**自动插入**灰色圆角日期分割线:
- `今天`(今天的全部消息前)
- `昨天`(昨天的全部消息前)
- `YYYY-MM-DD`(更早的)
> 分割线**不存盘**,是按消息 `ts` 现场算的,所以不用管它,刷新/重启会自动重算。
### 3.1 消息类型与颜色
| 类型 | 来源 | 样式 |
|---|---|---|
| `user` | 你输入 | 右对齐气泡 |
| `assistant` | LLM 回复 | 左对齐气泡 |
| `reminder` | 系统定时提醒 / 手动触发 | 绿松石色气泡(**带 🪧 action_type**) |
| `screenshot` | 截图预览(**不持久化**) | 中央居中,带"取消/发送给AI"按钮 |
| `system` | 错误提示(如 `[401] API key 无效`) | 灰色文字 |
### 3.2 思考气泡
如果 LLM 是 DeepSeek / QwQ / o1 这类带推理的模型,回复里会含 `...` 块。
- **自动**折叠为 `💭 思考过程 ▶` 小标签
- **点击**展开看到完整推理
- 支持多个 `` 块拼接、容错未闭合块、`` 标签
- 展开后**再次点击**收起
### 3.3 截图气泡的生命周期
```
[桌宠身体被点击] → 后端截图 → emit('ezvibe:screenshot-captured')
→ 前端 save_chat_image 存盘
→ 推一条 role='screenshot' 消息(含 imagePath / imagePngPath)
→ 渲染为带输入框和"取消/发送给AI"按钮的预览气泡
┌─────┴─────┐
[点"取消"] [输入文字 → 点"发送给AI"]
│ │
└─→ 立刻 removeMessage,立即消失
└─→ 立刻 removeMessage
└─→ 推一条 role='user' (含 imagePath + 文字)
└─→ 调 invoke('chat', { message, imagePath })
└─→ LLM 回复后推一条 role='assistant'
```
> ⚠️ screenshot 消息**三层防御**不写盘:
> 1. 前端 `removeMessage` 顺手清 `persistTimers` 里的 debounce
> 2. 后端 `append_message` 拒绝 `role=screenshot`
> 3. 后端 `load_session_messages` 跳过 `role=screenshot` 行
> 所以即使你狂点也不会"复活"。
### 3.4 截图发送给 AI 的限制
- 图片后缀必须是 `.png` / `.jpg` / `.jpeg`
- 路径**必须**是 `sessions//images/xxx` 形式(前端用 `appDataDir() + join` 拼绝对路径)
- **拒绝**绝对路径 / 路径遍历(`..`) / 非法 session id
- 文件大小上限 **20 MB**(防止 OOM)
- 给 LLM 用的是 **JPEG 预览**(80% 质量,几百 KB),不是 PNG 原图 — LLM 视觉够用且体积小 10 倍
## 4. 输入框
```
[ 多行文本框 (Enter发送 / Shift+Enter换行) ] [发送]
```
| 行为 | 说明 |
|---|---|
| **Enter** | 发送(前提:输入非空 且 LLM 已配置) |
| **Shift+Enter** | 换行 |
| 空消息 | 发送按钮 disabled |
| LLM 未配置 | placeholder 提示「请先到配置中心填写 API key」 |
| 思考中(loading) | 整个输入区 disabled,状态点变橙色,底部出现"桌宠正在思考…" |
## 5. 多会话管理
### 5.1 新建会话
点 + → 新会话立刻出现在下拉里,自动切过去,标题默认 `对话 MM-DD HH:MM`。
### 5.2 切换会话
点下拉选别的会话。**切换时自动加载**该会话的所有历史消息(JSONL 读盘)。
### 5.3 自动标题
- 触发条件:**消息数 ≥ 5** 且 **从未生成过标题**
- 时机:前端检测到 `append_message` 返回的新 `msg_count` 达到阈值时,调 `check_title_trigger` + `generate_session_title`
- 实现:异步调 LLM 总结前 5 条消息,输出 ≤ 20 字符短标题(中文 ≤ 12 字 / 英文 ≤ 20 字)
- 失败降级:LLM 报错就保持默认 `对话 MM-DD HH:MM`,下次还满足条件会再试
### 5.4 重命名
点 ✎ → 顶部弹输入框 → 输入新标题(≤ 40 字符)→ 回车确认。Esc 取消。
### 5.5 删除
点 🗑 → 确认弹窗 → **整个会话目录** + 索引条目都删(**包括该会话的所有截图**)。
### 5.6 提醒中心(伪会话)
- 标题固定 `🔔 提醒中心`,ID 固定 `__reminders__`
- **不可重命名 / 不可删除 / 不参与 LLM 自动标题**
- 所有定时触发的健康提醒**自动写入这个会话**(后端直接落盘,不依赖前端)
- 来新提醒时**自动跳转**到提醒中心视图(即使你正在看别的会话)
## 6. 搜索
### 6.1 当前会话搜索
- 点 🔍 展开第二行工具栏 → 输入关键字
- 即时过滤(输入就过滤)
- 命中片段在气泡里以**黄色背景高亮**(`` 标签)
- 切回空输入恢复显示全部
### 6.2 全局搜索
- 点工具栏 **🌐 全部** 按钮 → 弹出模态框
- 列出所有会话中**包含该关键字**的消息
- 每条命中显示上下文(命中前 20 字 + 命中词 + 命中后 40 字)
- **点任意命中** → 切到那个会话并关弹窗
> 全文搜索是**字面量匹配**(`text.find()`),不模糊不忽略大小写。中文按字节匹配。
## 7. 导出
### 7.1 格式
| 格式 | 文件名 | 内容 |
|---|---|---|
| **Markdown** | `-.md` | 标题 + 元信息 + 每条消息带 emoji 角色(👤/🤖/🔔/📸/⚙️)+ 时间戳 + 文本 + 图片相对路径 |
| **JSON** | `-.json` | pretty-printed JSON,含 `session: {meta}` 和 `messages: [...]` 数组 |
### 7.2 导出位置
**浏览器默认下载目录**。`ExportPayload.suggested_filename` 字段前端会直接用。
### 7.3 提醒中心能不能导出?
**能**。但因为是伪会话,导出内容主要是各种 `reminder` 类型的消息。
## 8. 常见疑问
**Q: 我能同时开几个聊天窗口吗?**
A: 不能。聊天面板是桌宠窗口的一部分(右侧栏),不是独立窗口。多个对话 = 多个会话(用下拉切换)。
**Q: 对话历史存在哪?**
A: 见 [08-数据存储与备份.md](08-数据存储与备份.md)。
**Q: 我清空了 localStorage 会不会丢?**
A: 不会。localStorage 没用,所有会话数据在磁盘。
**Q: 我能编辑 / 删除某条已发送消息吗?**
A: 不能。当前版本没有编辑或单条删除功能(只能删除整个会话)。
---
下一步:[06-健康提醒.md](06-健康提醒.md)。