`,避免 overflow/事件边界问题
- **健康气泡不显示**(用户报告):scheduler 间隔是 45min,且 22:00-08:00 静默时段抑制;改成 90s/180s + 关闭静默时段,加手动触发按钮
---
## 2. 架构总览(终态)
```
┌─────────────────────────────────────────────────────┐
│ Frontend (live2d.html → index.vue) │
│ ┌──────────────┐ ┌────────────────────────────┐ │
│ │ Live2D Canvas│ │ ChatPanel.vue │ │
│ │ (左,可收缩) │ │ - 状态条 + 🔔/ ?/× 按钮 │ │
│ │ │ │ - MessageBubble 流 │ │
│ │ │ │ ├─ user / assistant │ │
│ │ │ │ ├─ system (错误) │ │
│ │ │ │ └─ reminder (绿松石) │ │
│ │ │ │ └─ 含
折叠 │ │
│ │ │ │ - 输入框 + 发送 │ │
│ └──────────────┘ └────────────────────────────┘ │
│ ↓ invoke('chat', { message }) │
│ ↓ invoke('get_llm_status') │
│ ↓ listen('ezvibe:reminder') │
└─────────────────────────────────────────────────────┘
↓ Tauri IPC
┌─────────────────────────────────────────────────────┐
│ Rust Backend (src-tauri/src/) │
│ commands::chat → brain.think(UserMessage) │
│ → OpenAiProvider / AnthropicProvider │
│ → reqwest HTTP → 云端 API │
│ │
│ commands::get_llm_status → 读 AppConf 返回状态 │
│ 60s tokio loop → scheduler → brain.think(System) │
│ → FallbackCache → emit reminder │
└─────────────────────────────────────────────────────┘
```
---
## 3. 完整文件清单
### 3.1 新增
| 路径 | 行数 | 作用 |
|---|---|---|
| `src-tauri/src/modules/openai.rs` | ~190 | OpenAI 兼容 LLM Provider |
| `src-tauri/src/modules/anthropic.rs` | ~200 | Anthropic Claude Provider |
| `src/live2d/hooks/useChat.ts` | ~140 | 聊天状态管理 + reminder 监听 |
| `src/live2d/components/ChatPanel.vue` | ~280 | 聊天面板 UI |
| `src/live2d/components/MessageBubble.vue` | ~190 | 单条消息气泡(含思考折叠) |
| `docs/PLAN-CHAT.md` | ~250 | 早期设计稿 |
| `docs/impl/llm-chat-implementation.md` | ← 本文 | |
### 3.2 修改
| 路径 | 关键改动 |
|---|---|
| `src-tauri/Cargo.toml` | 删 `llama_cpp_rs`,加 `reqwest` (json + rustls-tls) |
| `src-tauri/src/utils.rs` | 删 `llm_model_dir` / `default_llm_model_path` / `resolve_llm_model_path` |
| `src-tauri/src/app/config.rs` | 加 `llm_provider` / `llm_model` / `llm_api_key` / `llm_base_url` 字段 |
| `src-tauri/src/app/commands.rs` | 删 `get_model_status`,加 `get_llm_status` |
| `src-tauri/src/main.rs` | 删 `NoopLLMProvider`,加工厂 + 旧 base_url 迁移(minimax.chat → 官方) |
| `src-tauri/src/modules/brain.rs` | `LLMProvider` trait 加 `is_loaded()` / `model_path()` 默认方法;`AgentBrain::provider()` 访问器;系统 prompt 加健康 tip 引导 |
| `src-tauri/src/modules/mod.rs` | 删 `pub mod llama_cpp`,加 `pub mod openai` / `pub mod anthropic` |
| `src-tauri/src/modules/scheduler.rs` | 间隔 45min→90s(remind_water)、60min→180s(remind_stretch);默认 `quiet_hours_enabled = false` |
| `src-tauri/tauri.conf.json` | 窗口 width: 215→600, height: 200→480, resizable: true |
| `src/components/Config.vue` | 加 LLM 设置区(4 字段 + 保存按钮) |
| `src/live2d/index.vue` | flex 左右布局 + 聊天面板 + 工具栏 🔔 按钮 + 模型居中 + 禁双击全屏 + 窗口 resize 重居中 |
### 3.3 删除
- `src-tauri/src/modules/llama_cpp.rs`(临时用过,已删)
- `~/.live2D/model/llm/`(用户移动过,API 路线后不再需要)
- 之前 patch 的 `~/.cargo/registry/.../llama_cpp_rs-0.3.0/build.rs`(crate 已不在依赖里,无影响)
---
## 4. 关键设计决策
### 4.1 为什么不把 `` 解析放后端
**问题**:DeepSeek / QwQ / o1 等模型输出含 `` 块,需要拆成可折叠气泡
**选择**:前端解析
**理由**:
- `brain.rs::strip_chatml_artifacts` 只剥 ChatML,`` 保留原样
- 前端 `MessageBubble.vue` 直接正则处理,无侵入
- 多种模型(DeepSeek/QwQ/o1/Claude extended thinking)都通用
- 不动 `BrainResponse` 字段(避免 commands.rs / hooks 连锁改动)
### 4.2 为什么 health reminder 在前端监听
**问题**:scheduler 60s 触发 reminder,要显示成气泡
**选择**:前端 `useChat.ts` 监听 `ezvibe:reminder` 事件
**理由**:
- 后端 `app_handle.emit("ezvibe:reminder", ...)` 已经在 `main.rs:60s 循环` 里发
- 前端只接收,符合"气泡属于 UI 层"的关注点分离
- 离后端近(如未来加表情播报)也方便
### 4.3 为什么 is_loaded / model_path 走 trait 默认
**问题**:`get_llm_status` 命令需要知道 provider 状态,但不想耦合到具体 provider
**选择**:在 `LLMProvider` trait 上加默认方法
**理由**:
- `OpenAiProvider` / `AnthropicProvider` 各自 override(key 非空即 ready)
- `NoopLLMProvider` 不存在了(已删)
- 任何未来新 provider 只要写自己的实现,不改 trait
### 4.4 为什么 in-memory provider 不监听 conf 变更
**问题**:用户在 UI 改 key 后,运行时如何生效?
**选择**:**必须重启**(in-memory provider 创建一次)
**理由**:
- 简单、不引入 RwLock