diff --git a/docs/02-系统架构.md b/docs/02-系统架构.md index b0f65a5..cbc1506 100644 --- a/docs/02-系统架构.md +++ b/docs/02-系统架构.md @@ -79,7 +79,7 @@ Browser Go Gateway STT LLM TTS | 模块 | 职责 | 关键实现 | |------|------|---------| | WebSocket Hub | 管理所有客户端连接,广播/定向推送 | goroutine per connection | -| Session Manager | 维护用户会话状态、对话历史 | Redis + TTL 过期策略 | +| Session Manager | 维护用户会话状态、对话历史 | Redis Hash + List,30 分钟 TTL(详见 `03-接口文档.md` 第五章) | | Model Router | 根据请求类型选择 AI 模型 | 规则引擎 + 成本阈值 | | AI Orchestrator | 编排多路 AI 调用(并行/串行) | context 取消 + 超时控制 | | Rate Limiter | 防止单用户过度消耗 API 额度 | 令牌桶算法 | diff --git a/docs/03-接口文档.md b/docs/03-接口文档.md index f3a0e94..e8e5380 100644 --- a/docs/03-接口文档.md +++ b/docs/03-接口文档.md @@ -533,7 +533,147 @@ func isSentenceEnd(delta string) bool { --- -## 五、数据模型 +## 五、Session Manager + +WebSocket Handler 和 AI Orchestrator 之间的会话管理层。负责维护会话生命周期、对话上下文和配置状态。 + +### Redis 数据结构 + +每个会话在 Redis 中占 2 个 key: + +``` +session:{id}:meta → Hash (会话元数据) +session:{id}:history → List (对话历史) +``` + +**Hash — `session:{id}:meta`** + +| field | 类型 | 示例 | 说明 | +|-------|------|------|------| +| `session_id` | string | `"550e8400-..."` | 主键冗余 | +| `config.tts_enabled` | string | `"true"` | Redis Hash 值均为 string | +| `config.detail_level` | string | `"low"` | | +| `config.language` | string | `"zh-CN"` | | +| `created_at` | string | `"2026-06-13T10:00:00Z"` | RFC3339 | +| `last_active` | string | `"2026-06-13T10:05:30Z"` | 每次消息刷新 | +| `active_request_id` | string | `"uuid"` 或 `""` | 当前处理中的请求 ID,用于 interrupt | + +**List — `session:{id}:history`** + +每个元素是一条 JSON 序列化的 Message: + +```json +{"role":"user","content":"这是什么花?"} +{"role":"assistant","content":"这是一朵红色的玫瑰。"} +``` + +- `LPUSH` 新消息到左头(最新在前) +- `LRANGE 0 {limit-1}` 取最近 N 轮 +- `LTRIM 0 {max-1}` 限制总条数(默认保留最近 20 条 = 10 轮对话) + +### TTL 策略 + +| 场景 | TTL | 说明 | +|------|-----|------| +| 创建时 | 30 分钟 | `EXPIRE` 设置 | +| 每次收到消息 | 重置 30 分钟 | `EXPIRE` 刷新 | +| WebSocket 断开 | 不主动删 | 等自然过期,支持重连恢复 | +| 超过 30 分钟无活动 | 自动过期 | Redis 自动清理 meta + history | +| 显式销毁(REST API) | 立即 `DEL` | 两个 key 一起删 | + +### 接口定义 + +```go +// SessionManager 会话管理器。 +// WebSocket Handler 通过此接口操作会话,不直接接触 Redis。 +type SessionManager interface { + // Create 创建新会话,返回 session ID。 + Create(ctx context.Context, config models.SessionConfig) (string, error) + + // Get 获取会话(含 config)。不存在返回 ErrSessionNotFound。 + Get(ctx context.Context, sessionID string) (*models.Session, error) + + // UpdateConfig 更新会话配置(config 消息触发)。 + UpdateConfig(ctx context.Context, sessionID string, patch models.SessionConfigPatch) error + + // GetHistory 获取最近 N 轮对话历史(供 Orchestrator 构建 LLM 上下文)。 + GetHistory(ctx context.Context, sessionID string, limit int) ([]models.Message, error) + + // AppendMessage 追加一条对话消息,同时刷新 TTL。 + AppendMessage(ctx context.Context, sessionID string, msg models.Message) error + + // SetActiveRequest 标记当前正在处理的请求 ID(interrupt 用)。 + SetActiveRequest(ctx context.Context, sessionID string, requestID string) error + + // ClearActiveRequest 清除活跃请求标记(请求完成或中断后)。 + ClearActiveRequest(ctx context.Context, sessionID string) error + + // Touch 刷新 TTL(心跳时调用)。 + Touch(ctx context.Context, sessionID string) error + + // Destroy 显式销毁会话(REST API DELETE 或连接断开清理)。 + Destroy(ctx context.Context, sessionID string) error +} +``` + +### WebSocket Handler 集成 + +```go +// query 分支 +case "query": + var msg models.WsQuery + json.Unmarshal(message, &msg) + + sessionMgr.Touch(ctx, sessionID) // 刷新 TTL + sessionMgr.SetActiveRequest(ctx, sessionID, msg.RequestID) // 标记活跃请求 + + history, _ := sessionMgr.GetHistory(ctx, sessionID, 20) // 获取对话上下文 + + go orchestrator.ProcessQuery(ctx, client, &msg, history) // 异步编排 + +// interrupt 分支 +case "interrupt": + reqID, _ := sessionMgr.GetActiveRequestID(ctx, sessionID) + if reqID != "" { + cancelFunc(reqID) // 取消对应 context + sessionMgr.ClearActiveRequest(ctx, sessionID) + } + +// 连接断开 +// 不调用 Destroy,让 session 自然过期(支持重连恢复) +``` + +### MVP 内存实现 + +联调阶段无 Redis 时,用同一接口的内存实现: + +```go +type InMemorySessionManager struct { + mu sync.RWMutex + sessions map[string]*sessionEntry +} + +type sessionEntry struct { + session models.Session + history []models.Message + activeReqID string +} +``` + +注入时根据配置切换: + +```go +var sessionMgr SessionManager +if cfg.Redis.Addr != "" { + sessionMgr = NewRedisSessionManager(redisClient, 30*time.Minute, 20) +} else { + sessionMgr = NewInMemorySessionManager() +} +``` + +--- + +## 六、数据模型 > 注:Go 和 TypeScript 的数据模型定义见下方。AI 服务层的 Go 模型见上方"AI 服务层接口"章节。 @@ -611,7 +751,7 @@ type ClientMessage = --- -## 六、扩展接口设计 +## 七、扩展接口设计 通过 Repository 接口隔离存储层,MVP 用内存实现,后续替换为数据库——业务逻辑零改动。 @@ -687,7 +827,7 @@ func NewApp(cfg *Config) *App { --- -## 七、错误码 +## 八、错误码 | 错误码 | 含义 | 客户端处理建议 | |--------|------|--------------| @@ -702,7 +842,7 @@ func NewApp(cfg *Config) *App { | `TTS_ERROR` | 语音合成失败 | 静默回退到纯文本回复 | | `INTERNAL_ERROR` | 服务端内部错误 | 提示用户重试 | -## 八、连接管理 +## 九、连接管理 **心跳机制**:客户端每 30 秒发送 `ping`,服务端回复 `pong`。超过 60 秒无 `ping`,服务端判定连接断开并清理会话资源。 diff --git a/docs/README.md b/docs/README.md index 8db40a7..99153d2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ CamTalk 是一款多模态实时 AI 视觉对话助手。用户通过摄像头 |------|------| | [01-项目概述](01-项目概述.md) | 项目目标、核心挑战、交付物 | | [02-系统架构](02-系统架构.md) | 三层架构、技术栈、核心交互流程、前后端模块、存储策略、部署架构 | -| [03-接口文档](03-接口文档.md) | WebSocket 协议、REST API、**AI 服务层接口(STT/LLM/TTS)**、**编排器设计**、数据模型、错误码、连接管理(**实现时首先阅读**) | +| [03-接口文档](03-接口文档.md) | WebSocket 协议、REST API、**AI 服务层接口**、**编排器设计**、**Session Manager(Redis)**、数据模型、错误码、连接管理(**实现时首先阅读**) | | [04-技术选型](04-技术选型.md) | 持久化层(PostgreSQL)和前端边缘处理层的选型对比与决策理由 | | [05-用户故事](05-用户故事.md) | P0/P1/P2 用户故事、验收标准、优先级决策依据 | | [06-语音交互](06-语音交互.md) | VAD → STT → LLM → TTS 全链路、延迟优化 |