feat: v2 版本 #206
11
CLAUDE.md
11
CLAUDE.md
@@ -73,7 +73,7 @@ go vet ./... # 静态分析
|
||||
- `POST /api/auth/logout` — 登出
|
||||
- `GET /api/conversations` — 对话列表
|
||||
- `POST /api/conversations` — 创建对话
|
||||
- `GET/PUT/PATCH/DELETE /api/conversations/:id` — 对话 CRUD
|
||||
- `GET/PATCH/DELETE /api/conversations/:id` — 对话详情/改标题/删除
|
||||
- `GET /api/conversations/:id/messages` — 获取对话消息
|
||||
|
||||
## 错误码
|
||||
@@ -84,18 +84,19 @@ go vet ./... # 静态分析
|
||||
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| `AuthPage` | 登录/注册表单 |
|
||||
| `LandingPage` | 未登录时的着陆页,内嵌 LoginModal 登录/注册弹窗 |
|
||||
| `AuthPage` | 登录/注册表单(备用) |
|
||||
| `CameraManager` | 摄像头流采集 |
|
||||
| `MicManager` | 麦克风音频采集 |
|
||||
| `EdgeProcessor` | VAD + 关键帧检测(Canvas 像素比较) |
|
||||
| `WebSocketManager` | WebSocket 连接生命周期管理 |
|
||||
| `ChatPanel` | 消息展示、流式回复、文本输入、场景选择 |
|
||||
| `VideoPreview` | 摄像头画面预览 |
|
||||
| `SessionSidebar` | 左侧抽屉式对话列表 |
|
||||
| `ConfigPanel` | 右侧抽屉式配置面板 |
|
||||
| `SessionSidebar` | 左侧抽屉式对话列表(搜索、重命名、删除、时间分组) |
|
||||
| `ConfigPanel` | 右侧抽屉式配置面板(主题、TTS、语言、场景、登出) |
|
||||
| `Toast` | 轻量通知提示 |
|
||||
|
||||
核心 Hook:`useVisionSession()` 封装一次完整的视觉对话会话。
|
||||
核心 Hook:`useVisionSession()` 封装一次完整的视觉对话会话。`useSessionList()` 管理对话列表 CRUD(通过 REST API)。
|
||||
|
||||
## 后端模块结构
|
||||
|
||||
|
||||
52
README.md
52
README.md
@@ -34,6 +34,8 @@ graph TB
|
||||
B2[Session Manager]
|
||||
B3[AI Orchestrator]
|
||||
B4[REST API]
|
||||
B5[Auth 模块]
|
||||
B6[Store 层]
|
||||
end
|
||||
|
||||
subgraph cloud[云端 AI 服务]
|
||||
@@ -54,9 +56,9 @@ graph TB
|
||||
|------|------|
|
||||
| 前端 | React 18, TypeScript, Vite, @ricky0123/vad-web |
|
||||
| 后端 | Go, Gin, gorilla/websocket, Viper, Zap |
|
||||
| STT | Deepgram(默认) / MiMo ASR |
|
||||
| LLM | GPT-4o(默认,通过 OpenAI 兼容接口可切换) |
|
||||
| TTS | OpenAI TTS(默认) / MiMo TTS |
|
||||
| STT | MiMo ASR(默认) / Deepgram |
|
||||
| LLM | DashScope qwen3-vl-plus(默认,通过 eino-ext OpenAI ChatModel 接入) |
|
||||
| TTS | MiMo TTS(默认) / OpenAI TTS |
|
||||
|
||||
## 项目结构
|
||||
|
||||
@@ -65,38 +67,49 @@ CamTalk/
|
||||
├── frontend/ # 浏览器客户端
|
||||
│ └── src/
|
||||
│ ├── components/ # UI 组件
|
||||
│ │ ├── LandingPage/ # 登录着陆页 + LoginModal
|
||||
│ │ ├── AuthPage/ # 登录/注册表单
|
||||
│ │ ├── CameraManager/ # 摄像头流采集
|
||||
│ │ ├── MicManager/ # 麦克风音频采集
|
||||
│ │ ├── EdgeProcessor/ # VAD + 关键帧检测
|
||||
│ │ ├── WebSocketManager/ # WS 连接管理
|
||||
│ │ ├── ChatPanel/ # 消息展示
|
||||
│ │ ├── VideoPreview/ # 摄像头画面预览
|
||||
│ │ ├── SessionSidebar/ # 对话历史侧边栏
|
||||
│ │ ├── ConfigPanel/ # 配置面板
|
||||
│ │ └── Toast/ # 通知提示
|
||||
│ ├── hooks/ # 自定义 Hooks
|
||||
│ │ ├── useVisionSession.ts # 核心会话 Hook
|
||||
│ │ ├── useSessionList.ts # 对话列表管理
|
||||
│ │ └── useObservationMode.ts # 观察模式
|
||||
│ ├── lib/ # 工具库
|
||||
│ │ ├── websocket.ts # WebSocket 连接管理
|
||||
│ │ ├── api.ts # REST API 客户端
|
||||
│ │ ├── auth.tsx # 认证上下文(JWT 管理)
|
||||
│ │ ├── audio.ts # 音频编码
|
||||
│ │ ├── ttsPlayer.ts # TTS 播放器
|
||||
│ │ ├── i18n/ # 国际化(zh-CN/en-US/ja-JP)
|
||||
│ │ └── sampling.ts # 采样策略
|
||||
│ └── types/ # TypeScript 类型定义
|
||||
├── backend/ # Go 网关
|
||||
│ ├── cmd/server/ # 入口
|
||||
│ └── internal/
|
||||
│ ├── ai/ # AI 服务抽象层
|
||||
│ │ ├── llm/ # LLM 服务(OpenAI 兼容)
|
||||
│ │ ├── stt/ # STT 服务(Deepgram/MiMo)
|
||||
│ │ └── tts/ # TTS 服务(OpenAI/MiMo)
|
||||
│ ├── orchestrator/ # AI 编排器(STT→LLM→TTS 管道)
|
||||
│ ├── session/ # 会话管理(Memory/Redis)
|
||||
│ │ ├── llm/ # LLM 提示词与场景
|
||||
│ │ ├── stt/ # STT 服务(MiMo/Deepgram)
|
||||
│ │ └── tts/ # TTS 服务(MiMo/OpenAI)
|
||||
│ ├── eino/ # Eino Graph 编排层(7 节点 DAG)
|
||||
│ ├── orchestrator/ # Orchestrator 接口
|
||||
│ ├── session/ # 会话管理(三级存储:Memory/Redis/PG)
|
||||
│ ├── store/ # 持久化层(Repository 接口 + PG/内存实现)
|
||||
│ ├── auth/ # 认证(JWT、bcrypt、中间件)
|
||||
│ ├── ws/ # WebSocket Handler
|
||||
│ ├── api/ # REST API
|
||||
│ ├── api/ # REST API(Auth/Conversation)
|
||||
│ ├── config/ # 配置管理
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── errors/ # 错误码
|
||||
│ └── logger/ # 日志
|
||||
├── migrations/ # 数据库迁移(嵌入式 SQL)
|
||||
├── docs/ # 设计文档
|
||||
└── CLAUDE.md # Claude Code 指引
|
||||
```
|
||||
@@ -106,7 +119,7 @@ CamTalk/
|
||||
### 前置条件
|
||||
|
||||
- Node.js >= 18
|
||||
- Go >= 1.24
|
||||
- Go >= 1.25
|
||||
|
||||
### 前端
|
||||
|
||||
@@ -147,20 +160,21 @@ go run ./cmd/server
|
||||
**客户端 → 服务端**:`query`、`config`、`interrupt`、`ping`
|
||||
**服务端 → 客户端**:`connected`、`stt_result`、`llm_chunk`、`llm_done`、`tts_audio`、`error`、`pong`
|
||||
|
||||
完整协议见 [docs/03-接口文档.md](docs/03-接口文档.md)。
|
||||
完整协议见 [docs/02-接口文档.md](docs/02-接口文档.md)。
|
||||
|
||||
## 文档
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [01-项目概述](docs/01-项目概述.md) | 项目目标与核心挑战 |
|
||||
| [02-系统架构](docs/02-系统架构.md) | 三层架构、技术栈、部署方案 |
|
||||
| [03-接口文档](docs/03-接口文档.md) | WebSocket 协议、REST API、配置管理 |
|
||||
| [04-技术选型](docs/04-技术选型.md) | AI 服务栈、持久化层、前端边缘处理选型 |
|
||||
| [05-用户故事](docs/05-用户故事.md) | 用户场景与优先级 |
|
||||
| [06-语音交互](docs/06-语音交互.md) | VAD → STT → LLM → TTS 全链路 |
|
||||
| [07-视觉理解](docs/07-视觉理解.md) | 帧采样、关键帧检测、多模态输入 |
|
||||
| [08-成本控制](docs/08-成本控制.md) | 采样策略、端云协同、模型分级 |
|
||||
| [01-架构设计](docs/01-架构设计.md) | 三层架构、技术栈、数据库设计、部署方案 |
|
||||
| [02-接口文档](docs/02-接口文档.md) | WebSocket 协议、REST API、AI 服务层、编排器、配置管理 |
|
||||
| [03-技术选型](docs/03-技术选型.md) | AI 服务栈、持久化层、前端边缘处理选型 |
|
||||
| [04-用户故事](docs/04-用户故事.md) | 用户场景与优先级 |
|
||||
| [05-语音交互](docs/05-语音交互.md) | VAD → STT → LLM → TTS 全链路 |
|
||||
| [06-视觉理解](docs/06-视觉理解.md) | 帧采样、关键帧检测、多模态输入 |
|
||||
| [07-成本控制](docs/07-成本控制.md) | 采样策略、端云协同、模型分级 |
|
||||
| [08-功能创意](docs/08-功能创意.md) | 功能创意与规划 |
|
||||
| [对话历史技术设计](docs/conversation-history-technical-design.md) | 对话历史功能的前端技术方案 |
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ graph LR
|
||||
| 模块 | 职责 |
|
||||
|------|------|
|
||||
| WebSocket Handler | 管理客户端连接生命周期,JWT 认证,conversation_id 恢复,单播消息推送 |
|
||||
| Session Manager | 维护用户会话状态、对话历史。Memory(默认)/ Redis(可切换),30 分钟 TTL,Write-Through 到 PG |
|
||||
| Session Manager | 维护用户会话状态、对话历史。三级存储(Memory → Redis → PostgreSQL),30 分钟 TTL,Write-Through 到 PG |
|
||||
| Eino 编排层 | 基于 CloudWeGo Eino Graph 的声明式 AI 编排。7 节点 DAG(STT→History→ChatModel→Msg2Str→Splitter→TTS→Done),Stream 模式调用,Callback 实现 LLM token 实时推送 |
|
||||
| AI Orchestrator | `EinoOrchestrator` 适配器,包装 Eino Graph 实现 `Orchestrator` 接口。context 取消 + 超时控制 |
|
||||
| AI Service Layer | AI 服务抽象层,多 provider 支持(Deepgram/MiMo/OpenAI 等) |
|
||||
@@ -216,18 +216,19 @@ graph LR
|
||||
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| AuthPage | 登录/注册表单 |
|
||||
| LandingPage | 未登录时的着陆页(营销展示),内嵌 LoginModal 登录/注册弹窗 |
|
||||
| AuthPage | 登录/注册表单(备用,已被 LandingPage + LoginModal 替代) |
|
||||
| CameraManager | 摄像头流采集 |
|
||||
| MicManager | 麦克风音频采集 |
|
||||
| EdgeProcessor | VAD + 关键帧检测(Canvas 像素比较) |
|
||||
| WebSocketManager | WS 连接生命周期管理 |
|
||||
| ChatPanel | 消息展示、流式回复、文本输入、场景选择 |
|
||||
| VideoPreview | 摄像头画面预览 |
|
||||
| SessionSidebar | 左侧抽屉式对话列表(搜索、重命名、删除) |
|
||||
| ConfigPanel | 右侧抽屉式配置面板(主题、TTS 开关、detail level、语言、场景、账户) |
|
||||
| SessionSidebar | 左侧抽屉式对话列表(搜索、重命名、删除、时间分组) |
|
||||
| ConfigPanel | 右侧抽屉式配置面板(主题、TTS 开关、detail level、语言、场景、登出) |
|
||||
| Toast | 轻量通知提示(3 秒自动消失) |
|
||||
|
||||
核心 Hook:`useVisionSession()` 封装一次完整的视觉对话会话(摄像头、VAD、WebSocket、消息状态、认证、场景模式)。
|
||||
核心 Hook:`useVisionSession()` 封装一次完整的视觉对话会话(摄像头、VAD、WebSocket、消息状态、认证、场景模式)。`useSessionList()` 通过 REST API 管理对话列表 CRUD(列表、创建、删除、重命名、加载消息)。
|
||||
|
||||
### 前端会话状态模型(三态)
|
||||
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
## CamTalk 对话历史功能 — Bug 分析与修复方案
|
||||
|
||||
### 一、整体架构现状
|
||||
|
||||
当前对话历史系统存在一个**根本性的架构缺陷**:前端和后端各自维护了一套完全独立的会话管理系统,两者之间从未同步。
|
||||
|
||||
**前端**:`useSessionList` Hook + `localStorage` 管理会话列表和消息存储。会话 ID 由前端 `uuid` 生成,消息通过 `localStorage` 持久化。
|
||||
|
||||
**后端**:`MemoryManager` (内存) + `PgSessionRepository` / `PgMessageRepository` (PostgreSQL) 管理会话和消息。会话 ID 由后端 `uuid.New()` 生成。
|
||||
|
||||
前端 `api.ts` 中没有任何对话相关的 REST API 调用,后端提供的 `/api/conversations` 全套接口(List / Create / Get / Patch / Delete / GetMessages)完全未被前端使用。
|
||||
|
||||
---
|
||||
|
||||
### 二、Bug 清单
|
||||
|
||||
#### P0 — 严重级别
|
||||
|
||||
**Bug 1:前后端会话系统完全脱节**
|
||||
|
||||
前端创建会话(`useSessionList.createSession`)只在 localStorage 中写入一条 `SessionSummary`,后端完全不知道这个会话的存在。后端在 WebSocket 连接时创建的会话(`ws/handler.go` L148)有独立的 ID,前端也无法感知。两套 ID 体系互不关联,导致:
|
||||
|
||||
- 前端切换/删除会话无法影响后端
|
||||
- 后端消息持久化到 PG 但前端无法读取
|
||||
- 对话历史不能跨设备、跨浏览器同步
|
||||
- 清除浏览器数据后所有历史丢失
|
||||
|
||||
**Bug 2:活跃对话中创建/切换会话导致后端消息写入错误会话**
|
||||
|
||||
复现步骤:
|
||||
1. 用户在会话 A 中正在对话(WebSocket 已连接,后端 sessionID = A)
|
||||
2. 用户点击"新建对话"
|
||||
3. 前端 `handleNewSession` 调用 `createSession()` 创建前端会话 B,调用 `setMessages([])` 清空 UI
|
||||
4. 由于 `connectionStatus === "connected"`,调用 `stopSession()` 断开 WebSocket
|
||||
5. 用户在新 UI 中发送消息,前端显示在"新对话"下
|
||||
6. 但 WebSocket 重连后,后端创建了一个**全新的**会话 C
|
||||
|
||||
结果:前端认为是会话 B,后端实际是会话 C。如果 `stopSession` 未执行(连接状态判断时序问题),消息甚至会写入旧会话 A。
|
||||
|
||||
**Bug 3:刷新页面后 activeSessionId 丢失,消息无法自动保存**
|
||||
|
||||
`useSessionList` 中 `activeSessionId` 初始值为 `null`,且不会从 localStorage 恢复:
|
||||
|
||||
```typescript
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||
```
|
||||
|
||||
初始化逻辑(`App.tsx` L122-129)仅在 `sessions.length === 0` 时调用 `createSession()`。对于回访用户(sessions 不为空),`activeSessionId` 保持 `null`。
|
||||
|
||||
自动保存的 `useEffect`(L136-140)需要 `activeSessionId` 非 null:
|
||||
|
||||
```typescript
|
||||
if (activeSessionId && messages.length > 0) {
|
||||
persistSession(activeSessionId, messages);
|
||||
}
|
||||
```
|
||||
|
||||
结果:回访用户如果不点击侧边栏选择会话,所有新消息不会被持久化,刷新页面即丢失。
|
||||
|
||||
#### P1 — 重要级别
|
||||
|
||||
**Bug 4:切换会话时强制断开 WebSocket,用户体验差**
|
||||
|
||||
`handleSelectSession` 和 `handleNewSession` 都调用 `stopSession()`,而 `stopSession` 会断开 WebSocket 连接。每次切换会话都需要重新建立连接(TCP 握手 + JWT 认证 + VAD 初始化),增加约 1-3 秒延迟。
|
||||
|
||||
正确做法应该是在切换会话时保持 WebSocket 连接,仅在后端切换 sessionID(通过发送 `conversation_id` 参数重连,或者在协议中增加切换会话的消息类型)。
|
||||
|
||||
**Bug 5:前端 historyRef 是无效的死代码**
|
||||
|
||||
`useVisionSession` 中的 `historyRef`(L48)被维护但从未被实际使用:
|
||||
|
||||
```typescript
|
||||
const historyRef = useRef<Array<{ role: string; content: string }>>([]);
|
||||
```
|
||||
|
||||
它被 push(`llm_done` 时 L258、`sendTextMessage` 时 L466、`interrupt` 时 L417),但从未被读取或发送到后端。前端的 LLM 上下文完全由后端 `session.Manager.GetHistory` 独立管理。这段代码增加了维护负担却没有任何功能价值。
|
||||
|
||||
**Bug 6:VAD 语音输入时用户消息未加入 historyRef**
|
||||
|
||||
`onSpeechEnd` 回调(L186-228)添加了用户消息到 `messages` state,但从未 push 到 `historyRef`。同样,`stt_result` 处理器(L236-249)更新消息文本后也未同步到 `historyRef`。
|
||||
|
||||
虽然 `historyRef` 本身是死代码(Bug 5),但如果未来要利用它,这个遗漏会造成语音消息在前端历史中缺失。
|
||||
|
||||
**Bug 7:观察模式消息未加入 historyRef**
|
||||
|
||||
`useObservationMode` 的 `onChange` 回调(L82-107)添加了用户消息但未 push 到 `historyRef`。同 Bug 6。
|
||||
|
||||
#### P2 — 一般级别
|
||||
|
||||
**Bug 8:ChatPanel 使用数组 index 作为 React key**
|
||||
|
||||
```tsx
|
||||
{messages.map((msg, index) => (
|
||||
<div key={index} ...>
|
||||
```
|
||||
|
||||
当消息列表动态变化时(如 STT 结果更新替换了占位消息),使用 index 作为 key 可能导致 React 无法正确 diff,出现闪烁或渲染异常。应使用稳定唯一的 ID(如 `timestamp` 或生成 UUID)。
|
||||
|
||||
**Bug 9:后端 AppendMessage 中 tokensUsed 始终为 0**
|
||||
|
||||
`MemoryManager.AppendMessage` 异步写 PG 时硬编码 `tokensUsed` 为 0:
|
||||
|
||||
```go
|
||||
if err := m.msgRepo.SaveMessage(context.Background(), sessionID, msg, 0); err != nil {
|
||||
```
|
||||
|
||||
`WsLLMDone` 中的 `tokens_used` 信息未被传递到持久化层,导致 PG 中所有消息的 token 统计均为 0。
|
||||
|
||||
**Bug 10:后端 WS Handler 与 Eino 编排器重复获取历史**
|
||||
|
||||
`handler.go` L240 获取了 `history` 并传给 `ProcessQuery`,但 `ProcessQuery`(`adapter.go`)内部并未使用这个参数。Eino Graph 的 History 节点(`nodes_history.go`)会自己重新调用 `sessionMgr.GetHistory`。传入的 `history` 参数被浪费了一次查询。
|
||||
|
||||
**Bug 11:后端 GetMessages 内存 fallback 的 beforeID 语义不一致**
|
||||
|
||||
PostgreSQL 实现中 `beforeID` 是消息 ID 游标(`WHERE id < $2`),而内存 fallback 将其当作数组索引偏移量:
|
||||
|
||||
```go
|
||||
if beforeID > 0 && int(beforeID) <= total {
|
||||
allMessages = allMessages[:beforeID]
|
||||
}
|
||||
```
|
||||
|
||||
两种实现的语义完全不同,切换存储后端时分页行为会不一致。
|
||||
|
||||
---
|
||||
|
||||
### 三、修复方案
|
||||
|
||||
#### 方案核心思路
|
||||
|
||||
将前端会话管理从 localStorage 迁移到后端 API,实现单一数据源。前端变为"薄客户端",会话 CRUD 和消息持久化全部走后端 `/api/conversations` 接口。
|
||||
|
||||
#### Phase 1:前端对接后端 API(解决 P0 Bug 1/2/3)
|
||||
|
||||
**1.1 在 api.ts 中增加对话 API 封装**
|
||||
|
||||
```typescript
|
||||
// 新增对话 API
|
||||
export async function listConversations(token: string, page = 1, size = 20) { ... }
|
||||
export async function createConversation(token: string, config?: SessionConfig) { ... }
|
||||
export async function getConversationMessages(token: string, id: string) { ... }
|
||||
export async function deleteConversation(token: string, id: string) { ... }
|
||||
export async function renameConversation(token: string, id: string, title: string) { ... }
|
||||
```
|
||||
|
||||
**1.2 重写 useSessionList Hook**
|
||||
|
||||
将所有 CRUD 操作从 localStorage 切换到后端 API:
|
||||
|
||||
- `createSession` → `POST /api/conversations`
|
||||
- `deleteSession` → `DELETE /api/conversations/:id`
|
||||
- `renameSession` → `PATCH /api/conversations/:id`
|
||||
- `selectSession` → `GET /api/conversations/:id/messages`
|
||||
- 初始化时 → `GET /api/conversations` 加载列表
|
||||
- 移除 `saveSessionMessages` / `loadSessionMessages` 等 localStorage 操作
|
||||
- 将 `activeSessionId` 持久化到 localStorage(仅用于恢复选中状态)
|
||||
|
||||
**1.3 初始化逻辑修复**
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
if (!initializedRef.current) {
|
||||
initializedRef.current = true;
|
||||
if (sessions.length === 0) {
|
||||
createSession();
|
||||
} else {
|
||||
// 恢复上次选中的会话
|
||||
const lastId = localStorage.getItem('camtalk:last_active_session');
|
||||
if (lastId && sessions.find(s => s.id === lastId)) {
|
||||
setActiveSessionId(lastId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [sessions.length, createSession]);
|
||||
```
|
||||
|
||||
#### Phase 2:WebSocket 会话切换(解决 P0 Bug 2, P1 Bug 4)
|
||||
|
||||
**2.1 WebSocket 连接增加 conversation_id 参数**
|
||||
|
||||
后端已支持 `conversation_id` 查询参数(`handler.go` L126-133),前端需要在 `connect` 时传入当前会话 ID:
|
||||
|
||||
```typescript
|
||||
connect(token?: string, conversationId?: string): void {
|
||||
const params = new URLSearchParams();
|
||||
if (token) params.set('token', token);
|
||||
if (conversationId) params.set('conversation_id', conversationId);
|
||||
const url = `${WS_URL}?${params.toString()}`;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**2.2 切换会话时保持连接**
|
||||
|
||||
在 `handleSelectSession` 中,不再调用 `stopSession()`,而是:
|
||||
1. 保存当前会话消息到后端(如果需要)
|
||||
2. 断开当前 WebSocket
|
||||
3. 用新会话 ID 重新连接
|
||||
|
||||
或者更优方案:在 WebSocket 协议中增加 `switch_session` 消息类型,允许在保持连接的情况下切换后端会话。
|
||||
|
||||
#### Phase 3:清理前端冗余代码(解决 P1 Bug 5/6/7, P2 Bug 8)
|
||||
|
||||
**3.1 移除 historyRef**
|
||||
|
||||
删除 `useVisionSession` 中的 `historyRef` 及其所有 push 操作。前端不再维护独立的 LLM 上下文历史,完全依赖后端。
|
||||
|
||||
**3.2 消息列表使用稳定 key**
|
||||
|
||||
将 `ChatMessage` 类型增加 `id` 字段(UUID),在创建消息时生成,用作文本 diff 和 React key。
|
||||
|
||||
```typescript
|
||||
export interface ChatMessage {
|
||||
id: string; // 新增
|
||||
role: "user" | "assistant" | "system";
|
||||
content: string;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### Phase 4:后端修复(解决 P2 Bug 9/10/11)
|
||||
|
||||
**4.1 传递 tokensUsed 到持久化层**
|
||||
|
||||
修改 `AppendMessage` 接口,增加 `tokensUsed` 参数;或在 `EinoOrchestrator.ProcessQuery` 中,在 `llm_done` 后单独调用一次 `UpdateMessageMeta` 更新 token 信息。
|
||||
|
||||
**4.2 移除 WS Handler 中多余的 GetHistory 调用**
|
||||
|
||||
删除 `handler.go` L240 的 `history` 获取,同时从 `ProcessQuery` 签名中移除 `history` 参数。
|
||||
|
||||
**4.3 统一 GetMessages beforeID 语义**
|
||||
|
||||
内存 fallback 中改为基于消息序号的偏移量,或直接移除内存 fallback(生产环境始终使用 PG)。
|
||||
|
||||
---
|
||||
|
||||
### 四、实施优先级
|
||||
|
||||
| 优先级 | 修复项 | 预估工作量 |
|
||||
|--------|--------|-----------|
|
||||
| P0 | 前端对接后端 API + 初始化修复 | 2-3 天 |
|
||||
| P0 | WebSocket 会话切换 | 1-2 天 |
|
||||
| P1 | 清理 historyRef 死代码 | 0.5 天 |
|
||||
| P2 | React key + tokensUsed + GetMessages | 1 天 |
|
||||
|
||||
总计约 5-7 天可完成全部修复。Phase 1 是核心,完成后对话历史功能即可正常工作。
|
||||
Reference in New Issue
Block a user