From dca37f3e482bf3b7c69613a0373228c6ad8a003f Mon Sep 17 00:00:00 2001 From: hhs <386998068@qq.com> Date: Fri, 19 Jun 2026 14:58:43 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=90=8C=E6=AD=A5=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E4=B8=8E=E4=BB=A3=E7=A0=81=E5=AE=9E=E7=8E=B0=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 02-系统架构: Redis/PostgreSQL 标注已实现,模块表新增 Auth/Store/Migrations,更新表设计和前端组件 - 03-接口文档: config 新增 scenario 字段,Manager 接口补全 UpdateTitle/ListByUser,配置结构体同步,扩展接口替换为实际 Repository - 04-技术选型: 持久化层标注已实现 - 06-语音交互: TTS Voice 更正为 mimo_default - 11-持久化与用户系统设计: 所有 Phase 标记完成 - PLAN_BACKEND/PLAN_USER_MODULE: 标记完成状态 - README: 新增实现状态总览,补充文档索引 --- docs/02-系统架构.md | 126 +++++++-------- docs/03-接口文档.md | 261 +++++++++++++++++++++----------- docs/04-技术选型.md | 17 ++- docs/06-语音交互.md | 2 +- docs/11-持久化与用户系统设计.md | 61 ++++---- docs/PLAN_BACKEND.md | 18 ++- docs/PLAN_USER_MODULE.md | 20 +-- docs/README.md | 41 +++-- 8 files changed, 333 insertions(+), 213 deletions(-) diff --git a/docs/02-系统架构.md b/docs/02-系统架构.md index 0a2b316..85a24ee 100644 --- a/docs/02-系统架构.md +++ b/docs/02-系统架构.md @@ -34,8 +34,8 @@ | 语言 | Go | 高并发 goroutine 模型,适合长连接管理 | | HTTP 框架 | Gin | 高性能 HTTP 路由,中间件生态成熟 | | WebSocket | gorilla/websocket | Go 生态最成熟的 WebSocket 库 | -| 会话存储 | Redis(规划中) / Memory(MVP 默认) | 高速 KV 存储,MVP 阶段使用进程内存,可通过配置切换到 Redis | -| 持久化存储 | PostgreSQL(规划中) | 对话历史、用量统计、用户偏好(MVP 阶段未实现) | +| 会话存储 | Redis(已实现) / Memory(默认) | 高速 KV 存储,Memory 为默认实现,Redis 已实现可通过配置切换 | +| 持久化存储 | PostgreSQL(已实现) | 对话历史、用户数据、会话持久化。MemoryManager 支持 Write-Through 到 PG | | 配置管理 | Viper + godotenv | 支持 YAML + .env + 环境变量覆盖,详见 `03-接口文档.md` 第六章 | | 日志 | Zap | 高性能结构化日志 | @@ -76,18 +76,21 @@ Browser Go Gateway STT LLM TTS ## 后端模块 -| 模块 | 职责 | 关键实现 | -|------|------|---------| -| WebSocket Handler | 管理客户端连接生命周期,单播消息推送 | goroutine per connection | -| Session Manager | 维护用户会话状态、对话历史 | Memory(MVP 默认)/ Redis(可切换),30 分钟 TTL(详见 `03-接口文档.md` 第五章) | -| AI Orchestrator | 编排 STT→LLM→TTS 流式并行管道 | context 取消 + 超时控制 + 句子切分 | -| AI Service Layer | AI 服务抽象层(STT/LLM/TTS) | 多 provider 支持(Deepgram/MiMo/OpenAI 等) | -| REST API | 健康检查、会话管理端点 | Gin 路由 | -| Error Handler | 统一错误码定义与发送 | 错误码枚举 | -| Logger | 日志初始化封装 | Zap 结构化日志 | -| Models | 数据模型定义 | WebSocket 消息、会话、配置等 | -| Model Router | 根据请求类型选择 AI 模型(规划中) | 规则引擎 + 成本阈值 | -| Rate Limiter | 防止单用户过度消耗 API 额度(规划中) | 令牌桶算法 | +| 模块 | 职责 | 关键实现 | 状态 | +|------|------|---------|------| +| WebSocket Handler | 管理客户端连接生命周期,单播消息推送 | goroutine per connection,JWT 认证,conversation_id 恢复 | ✅ 已完成 | +| Session Manager | 维护用户会话状态、对话历史 | Memory(默认)/ Redis(可切换),30 分钟 TTL,Write-Through 到 PG(详见 `03-接口文档.md` 第五章) | ✅ 已完成 | +| AI Orchestrator | 编排 STT→LLM→TTS 流式并行管道 | context 取消 + 超时控制 + 句子切分 | ✅ 已完成 | +| AI Service Layer | AI 服务抽象层(STT/LLM/TTS) | 多 provider 支持(Deepgram/MiMo/OpenAI 等) | ✅ 已完成 | +| Auth | 用户认证与授权 | JWT (HS256) 双 token 轮转,bcrypt 密码哈希,Gin 中间件 | ✅ 已完成 | +| Store | 持久化存储层 | UserRepository / MessageRepository / SessionRepository,内存 + PostgreSQL 双实现 | ✅ 已完成 | +| REST API | 健康检查、认证、对话管理端点 | Gin 路由,输入校验,权限校验 | ✅ 已完成 | +| Error Handler | 统一错误码定义与发送 | 错误码枚举 | ✅ 已完成 | +| Logger | 日志初始化封装 | Zap 结构化日志 | ✅ 已完成 | +| Models | 数据模型定义 | WebSocket 消息、会话、配置、用户等 | ✅ 已完成 | +| Migrations | 数据库版本化迁移 | 嵌入式 SQL 文件,自动执行,版本跟踪 | ✅ 已完成 | +| Model Router | 根据请求类型选择 AI 模型 | 规则引擎 + 成本阈值 | 📋 规划中 | +| Rate Limiter | 防止单用户过度消耗 API 额度 | 令牌桶算法 | 📋 规划中 | AI Orchestrator 核心接口(`internal/orchestrator/orchestrator.go`): @@ -113,44 +116,31 @@ Pipeline 实现(`internal/orchestrator/pipeline.go`)流程: | 组件 | 职责 | |------|------| +| AuthPage | 登录/注册表单,前端校验,Tab 切换 | | CameraManager | 摄像头流采集 | | MicManager | 麦克风音频采集 | | EdgeProcessor | VAD + 关键帧检测(Canvas 像素比较) | | WebSocketManager | WS 连接生命周期管理 | -| ChatPanel | 消息展示 | +| ChatPanel | 消息展示、流式回复、文本输入、场景选择 | | VideoPreview | 摄像头画面预览 | -| ConfigPanel | 右侧抽屉式配置面板(主题、TTS 开关、detail level、语言) | +| SessionSidebar | 左侧抽屉式对话列表(搜索、重命名、删除) | +| ConfigPanel | 右侧抽屉式配置面板(主题、TTS 开关、detail level、语言、场景、账户) | | Toast | 轻量通知提示(3 秒自动消失) | -核心 Hook:`useVisionSession()` 封装一次完整的视觉对话会话(摄像头、VAD、WebSocket、消息状态)。 +核心 Hook:`useVisionSession()` 封装一次完整的视觉对话会话(摄像头、VAD、WebSocket、消息状态、认证、场景模式)。 ```typescript +// useVisionSession 核心职责(简化示意) function useVisionSession() { - const [messages, setMessages] = useState([]); - const wsRef = useWebSocket(`${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/ws`); - const videoRef = useRef(null); - const { captureFrame } = useCamera(videoRef); + // 组合:useCamera + useMicrophone + useVAD + useWebSocketManager + useObservationMode + // 管理:消息状态、流式回复、处理标志、配置、统计、模式 - const { isSpeaking } = useVAD({ - onSpeechEnd: async (audio) => { - const frame = captureFrame(); - wsRef.current?.send(JSON.stringify({ - type: "query", - image: frame.toDataURL("image/jpeg", 0.7), - audio: encodeAudio(audio) - })); - } - }); - - useEffect(() => { - wsRef.current?.on("message", (data) => { - const { text, audio } = JSON.parse(data); - setMessages(prev => [...prev, { role: "assistant", text }]); - if (audio) playAudio(audio); - }); - }, []); - - return { messages, videoRef, isSpeaking }; + // VAD onSpeechEnd: 捕获帧 + 音频 → 发送 query 消息 + // 服务端消息处理:stt_result / llm_chunk / llm_done / tts_audio / error + // 文本输入:sendTextMessage() 支持手动输入文字(跳过 STT) + // 场景模式:config 消息支持 scenario 字段(free_chat / interviewer / english_teacher 等) + // 打断:interrupt() 发送中断消息 + 停止 TTS + 保存部分回复 + // 认证:WebSocket 连接携带 JWT token,支持 conversation_id 恢复历史对话 } ``` @@ -158,40 +148,52 @@ function useVisionSession() { | 阶段 | 存储方案 | 持久化内容 | 理由 | |------|---------|-----------|------| -| MVP | Memory(进程内) | 无 | 快速验证核心功能,重启丢数据可接受。Redis 实现已就绪,可通过 `storage.driver` 配置切换 | -| 上线 | Redis + PostgreSQL | 对话历史、用户偏好、用量统计 | 用户需要查看历史,运营需要成本数据 | -| 规模化 | Redis + PG + 对象存储 | 图像帧、音频片段归档 | 大文件不适合存关系库 | +| 当前默认 | Memory(进程内) | 会话状态 + 对话历史 | 零依赖,快速启动。MemoryManager 支持 Write-Through 到 PG | +| 已实现 | Memory + PostgreSQL | 用户数据、对话历史、会话元数据 | 通过 `storage.driver: postgres` 启用,MemoryManager 注入 PG Repository | +| 已实现 | Redis(独立) | 会话状态 + 对话历史 | 通过配置切换到 RedisManager,适合多实例部署 | -冷热分离:Redis 存"热数据"(当前对话上下文,微秒级读写),PostgreSQL 存"冷数据"(历史记录)。 +冷热分离:Redis/Memory 存"热数据"(当前对话上下文,微秒级读写),PostgreSQL 存"冷数据"(历史记录)。MemoryManager 的 Write-Through 机制确保每次 AppendMessage 同时写入 PG,重启后可从 PG 恢复会话。 -### PostgreSQL 表设计 +### PostgreSQL 表设计(已实现) + +实际迁移文件位于 `backend/migrations/`,通过 `go:embed` 嵌入,启动时自动执行: ```sql -CREATE TABLE sessions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL, - created_at TIMESTAMPTZ DEFAULT now(), - updated_at TIMESTAMPTZ DEFAULT now() +-- 001_users.up.sql +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username VARCHAR(64) NOT NULL UNIQUE, + password_hash VARCHAR(256) NOT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() ); +CREATE TABLE refresh_tokens ( + id BIGSERIAL PRIMARY KEY, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash VARCHAR(256) NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() +); + +-- 002_messages.up.sql CREATE TABLE messages ( id BIGSERIAL PRIMARY KEY, - session_id UUID REFERENCES sessions(id), - role VARCHAR(16) NOT NULL, -- "user" | "assistant" + session_id UUID NOT NULL, + role VARCHAR(16) NOT NULL, content TEXT NOT NULL, - image_url TEXT, tokens_used INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT now() ); -CREATE TABLE usage_daily ( - user_id UUID NOT NULL, - date DATE NOT NULL, - llm_tokens BIGINT DEFAULT 0, - stt_seconds REAL DEFAULT 0, - tts_chars INTEGER DEFAULT 0, - estimated_cost NUMERIC(10,4) DEFAULT 0, - PRIMARY KEY (user_id, date) +-- 003_sessions.up.sql +CREATE TABLE sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title VARCHAR(128) DEFAULT '新对话', + config JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() ); ``` diff --git a/docs/03-接口文档.md b/docs/03-接口文档.md index 3f7d28f..1efe171 100644 --- a/docs/03-接口文档.md +++ b/docs/03-接口文档.md @@ -2,7 +2,7 @@ ## 概述 -前后端通信接口定义。以 WebSocket 承载实时对话,REST 端点支撑基础运维。**暂不实现持久化**,但通过 Repository 接口模式为后续扩展预留接入点。 +前后端通信接口定义。以 WebSocket 承载实时对话,REST 端点支撑基础运维。持久化已通过 PostgreSQL 实现,MemoryManager 支持 Write-Through 模式。 **设计原则**: - WebSocket 为主:所有对话数据走 WebSocket @@ -79,6 +79,7 @@ interface ConfigMessage { tts_enabled?: boolean; // 是否开启语音合成,默认 true detail_level?: "low" | "high"; // 图像精度,默认 "low" language?: string; // 交互语言,默认 "zh-CN" + scenario?: string; // 场景模式,可选值:free_chat / interviewer / english_teacher / debate / interpreter }; } ``` @@ -746,6 +747,20 @@ DELETE /api/sessions/{id} → 改用 DELETE /api/conversations/{id} | `/api/usage` | GET | 查询用量统计 | | `/api/users/{id}/preferences` | GET/PUT | 用户偏好管理 | +### 已实现的认证与对话端点 + +> 详见上方"认证接口"和"对话接口"章节。包括: +> - `POST /api/auth/register` — 注册 +> - `POST /api/auth/login` — 登录 +> - `POST /api/auth/refresh` — 刷新 Token +> - `POST /api/auth/logout` — 登出 +> - `GET /api/conversations` — 对话列表 +> - `POST /api/conversations` — 创建对话 +> - `GET /api/conversations/:id` — 对话详情 +> - `PATCH /api/conversations/:id` — 更新标题 +> - `DELETE /api/conversations/:id` — 删除对话 +> - `GET /api/conversations/:id/messages` — 历史消息 + --- ## 三、AI 服务层接口 @@ -992,8 +1007,8 @@ session:{id}:history → List (对话历史) // Manager 会话管理器接口。 // WebSocket Handler 通过此接口操作会话,不直接接触存储层。 type Manager interface { - // Create 创建新会话,返回 session ID。 - Create(ctx context.Context, config models.SessionConfig) (string, error) + // Create 创建新会话,关联 user_id,返回 session ID。 + Create(ctx context.Context, userID string, config models.SessionConfig) (string, error) // Get 获取会话(含 config)。不存在返回 ErrSessionNotFound。 Get(ctx context.Context, sessionID string) (*models.Session, error) @@ -1001,10 +1016,16 @@ type Manager interface { // UpdateConfig 更新会话配置(config 消息触发)。 UpdateConfig(ctx context.Context, sessionID string, patch models.SessionConfigPatch) error + // UpdateTitle 更新对话标题。 + UpdateTitle(ctx context.Context, sessionID string, title string) error + + // ListByUser 获取用户的对话列表(分页)。 + ListByUser(ctx context.Context, userID string, page, size int) ([]ConversationSummary, int, error) + // GetHistory 获取最近 N 轮对话历史(供 Orchestrator 构建 LLM 上下文)。 GetHistory(ctx context.Context, sessionID string, limit int) ([]models.Message, error) - // AppendMessage 追加一条对话消息,同时刷新 TTL。 + // AppendMessage 追加一条对话消息,同时刷新 TTL。首条 user 消息自动更新标题。 AppendMessage(ctx context.Context, sessionID string, msg models.Message) error // SetActiveRequest 标记当前正在处理的请求 ID(interrupt 用)。 @@ -1025,11 +1046,37 @@ type Manager interface { // ActiveCount 返回当前活跃会话数(健康检查用)。 ActiveCount() int } + +// ConversationSummary 对话列表项。 +type ConversationSummary struct { + ID string `json:"id"` + Title string `json:"title"` + LastMessage string `json:"last_message"` + MessageCount int `json:"message_count"` + UpdatedAt time.Time `json:"updated_at"` +} ``` ### WebSocket Handler 集成 ```go +// 连接建立(JWT 认证 + conversation_id 恢复) +func (h *Handler) HandleWS(c *gin.Context) { + tokenStr := c.Query("token") + claims, err := h.tokenManager.ValidateAccess(tokenStr) + // ... 认证失败返回 401 + + conversationID := c.Query("conversation_id") + if conversationID != "" { + sess, _ := h.sessionMgr.Get(c, conversationID) + if sess.UserID != claims.UserID { /* 返回 SESSION_NOT_FOUND */ } + sessionID = conversationID + } else { + sessionID, _ = h.sessionMgr.Create(c, claims.UserID, defaultConfig) + } + // ... 进入 WS 处理循环 +} + // query 分支 case "query": var msg models.WsQuery @@ -1055,9 +1102,9 @@ case "interrupt": // 不调用 Destroy,让 session 自然过期(支持重连恢复) ``` -### MVP 内存实现 +### 内存实现(默认) -联调阶段无 Redis 时,用同一接口的内存实现: +默认使用 `MemoryManager`,支持可选的 Write-Through 到 PostgreSQL: ```go type MemoryManager struct { @@ -1066,6 +1113,8 @@ type MemoryManager struct { ttl time.Duration maxHistory int stopCleaner chan struct{} + msgRepo store.MessageRepository // 可选,Write-Through + sessRepo store.SessionRepository // 可选,Write-Through } type sessionEntry struct { @@ -1076,6 +1125,10 @@ type sessionEntry struct { } ``` +**Write-Through 机制**:注入 `msgRepo` 和 `sessRepo` 后,每次 `AppendMessage` 同时写入 PostgreSQL,重启后可从 PG 恢复会话。`Create` 时同时写入 PG sessions 表。 + +**自动标题**:首条 user 消息时,如果 title 仍为 "新对话",自动更新为消息内容前 20 个字符。 + 注入时根据配置切换: ```go @@ -1083,7 +1136,14 @@ var sessionMgr session.Manager if cfg.Redis.Addr != "" { sessionMgr = session.NewRedisManager(redisClient, 30*time.Minute, 20) } else { - sessionMgr = session.NewMemoryManager(30*time.Minute, 20) + opts := []session.Option{} + if msgRepo != nil { + opts = append(opts, session.WithMessageRepository(msgRepo)) + } + if sessRepo != nil { + opts = append(opts, session.WithSessionRepository(sessRepo)) + } + sessionMgr = session.NewMemoryManager(30*time.Minute, 20, opts...) } ``` @@ -1110,6 +1170,7 @@ Viper 加载顺序:先读 `config.yaml`,再根据 `APP_ENV` 环境变量尝 type Config struct { App AppConfig `mapstructure:"app"` Server ServerConfig `mapstructure:"server"` + Session SessionConfig `mapstructure:"session"` Redis RedisConfig `mapstructure:"redis"` AI AIConfig `mapstructure:"ai"` Storage StorageConfig `mapstructure:"storage"` @@ -1123,10 +1184,19 @@ type AppConfig struct { } type ServerConfig struct { - Host string `mapstructure:"host"` // 默认 "0.0.0.0" - Port int `mapstructure:"port"` // 默认 8080 - ReadTimeout int `mapstructure:"read_timeout"` // 秒,默认 30 - WriteTimeout int `mapstructure:"write_timeout"` // 秒,默认 30 + Host string `mapstructure:"host"` // 默认 "0.0.0.0" + Port int `mapstructure:"port"` // 默认 8080 + ReadTimeout int `mapstructure:"read_timeout"` // 秒,默认 30 + WriteTimeout int `mapstructure:"write_timeout"` // 秒,默认 30 + HeartbeatInterval int `mapstructure:"heartbeat_interval"` // 秒,默认 30 + HeartbeatTimeout int `mapstructure:"heartbeat_timeout"` // 秒,默认 60 + ShutdownTimeout int `mapstructure:"shutdown_timeout"` // 秒,默认 10 + AllowedOrigins []string `mapstructure:"allowed_origins"` // 空表示允许所有 +} + +type SessionConfig struct { + TTL int `mapstructure:"ttl"` // 分钟,默认 30 + MaxHistory int `mapstructure:"max_history"` // 条数,默认 20 } type RedisConfig struct { @@ -1142,28 +1212,34 @@ type AIConfig struct { } type STTConfig struct { - Provider string `mapstructure:"provider"` // "deepgram" - APIKey string `mapstructure:"api_key"` - Model string `mapstructure:"model"` // 默认 "nova-2" - Endpoint string `mapstructure:"endpoint"` // 默认 "wss://api.deepgram.com/v1/listen" + Provider string `mapstructure:"provider"` // "deepgram" | "mimo" | "xiaomi" + APIKey string `mapstructure:"api_key"` + Model string `mapstructure:"model"` // 默认 "nova-2" + Endpoint string `mapstructure:"endpoint"` // 默认 "wss://api.deepgram.com/v1/listen" + Timeout int `mapstructure:"timeout"` // 秒,默认 5 + HTTPClientTimeout int `mapstructure:"http_client_timeout"` // 秒,默认 30 } type LLMConfig struct { - Provider string `mapstructure:"provider"` // "openai" - APIKey string `mapstructure:"api_key"` - Model string `mapstructure:"model"` // 默认 "gpt-4o" - Endpoint string `mapstructure:"endpoint"` // 默认 "https://api.openai.com/v1" - Timeout int `mapstructure:"timeout"` // 秒,默认 10 + Provider string `mapstructure:"provider"` // "openai" + APIKey string `mapstructure:"api_key"` + Model string `mapstructure:"model"` // 默认 "gpt-4o" + Endpoint string `mapstructure:"endpoint"` // 默认 "https://api.openai.com/v1" + Timeout int `mapstructure:"timeout"` // 秒,默认 10 + HTTPClientTimeout int `mapstructure:"http_client_timeout"` // 秒,默认 60 } type TTSConfig struct { - Provider string `mapstructure:"provider"` // "openai" - APIKey string `mapstructure:"api_key"` - Model string `mapstructure:"model"` // 默认 "tts-1" - Voice string `mapstructure:"voice"` // 默认 "alloy" - Speed float64 `mapstructure:"speed"` // 默认 1.0 - Endpoint string `mapstructure:"endpoint"` // 默认 "https://api.openai.com/v1" - Timeout int `mapstructure:"timeout"` // 秒,默认 5 + Provider string `mapstructure:"provider"` // "openai" | "mimo" | "xiaomi" + APIKey string `mapstructure:"api_key"` + Model string `mapstructure:"model"` // 默认 "tts-1" + Voice string `mapstructure:"voice"` // 默认 "mimo_default" + Speed float64 `mapstructure:"speed"` // 默认 1.0 + Endpoint string `mapstructure:"endpoint"` // 默认 "https://api.openai.com/v1" + Timeout int `mapstructure:"timeout"` // 秒,默认 5 + HTTPClientTimeout int `mapstructure:"http_client_timeout"` // 秒,默认 30 + OutputFormat string `mapstructure:"output_format"` // 默认 "mp3" + SampleRate int `mapstructure:"sample_rate"` // 默认 24000 } type StorageConfig struct { @@ -1195,6 +1271,13 @@ server: port: 8080 read_timeout: 30 write_timeout: 30 + heartbeat_interval: 30 + heartbeat_timeout: 60 + shutdown_timeout: 10 + +session: + ttl: 30 + max_history: 20 redis: addr: "localhost:6379" @@ -1206,18 +1289,24 @@ ai: provider: deepgram model: nova-2 endpoint: "wss://api.deepgram.com/v1/listen" + timeout: 5 + http_client_timeout: 30 llm: provider: openai model: gpt-4o endpoint: "https://api.openai.com/v1" timeout: 10 + http_client_timeout: 60 tts: provider: openai model: tts-1 - voice: alloy + voice: mimo_default speed: 1.0 endpoint: "https://api.openai.com/v1" timeout: 5 + http_client_timeout: 30 + output_format: mp3 + sample_rate: 24000 storage: driver: memory @@ -1360,6 +1449,7 @@ type SessionConfig struct { TTSEnabled bool `json:"tts_enabled"` DetailLevel string `json:"detail_level"` // "low" | "high" Language string `json:"language"` + Scenario string `json:"scenario"` // "free_chat" | "interviewer" | "english_teacher" | "debate" | "interpreter" } type QueryRequest struct { @@ -1416,6 +1506,7 @@ interface SessionConfig { ttsEnabled: boolean; detailLevel: "low" | "high"; language: string; + scenario: string; // "free_chat" | "interviewer" | "english_teacher" | "debate" | "interpreter" } interface ChatMessage { @@ -1499,79 +1590,79 @@ type ClientMessage = --- -## 八、扩展接口设计 +## 八、存储层接口设计 -通过 Repository 接口隔离存储层,MVP 用内存实现,后续替换为数据库——业务逻辑零改动。 +通过 Repository 接口隔离存储层,内存和 PostgreSQL 均已实现,业务逻辑零改动。 + +### UserRepository ```go -// HistoryRepository — 对话历史存储契约 -// MVP: 内存实现(session 内有效,断开即丢) -// 后续: PostgreSQL 实现 -type HistoryRepository interface { - SaveMessage(ctx context.Context, sessionID string, msg Message) error - GetMessages(ctx context.Context, sessionID string, limit int) ([]Message, error) -} - -// UsageRepository — 用量统计存储契约 -// MVP: 内存计数器 -// 后续: PostgreSQL 按天聚合 -type UsageRepository interface { - RecordUsage(ctx context.Context, sessionID string, usage UsageRecord) error - GetDailyUsage(ctx context.Context, userID string, days int) ([]UsageDaily, error) +// UserRepository 用户数据存储契约。 +// 内存实现:MemUserRepository(测试/开发用) +// PostgreSQL 实现:PgUserRepository +type UserRepository interface { + Create(ctx context.Context, username, passwordHash string) (string, error) + FindByUsername(ctx context.Context, username string) (*User, error) + FindByID(ctx context.Context, id string) (*User, error) + SaveRefreshToken(ctx context.Context, userID, tokenHash string, expiresAt time.Time) error + FindRefreshToken(ctx context.Context, tokenHash string) (string, error) + DeleteRefreshToken(ctx context.Context, tokenHash string) error + DeleteUserRefreshTokens(ctx context.Context, userID string) error } ``` -MVP 内存实现: +### MessageRepository ```go -type InMemoryHistory struct { - mu sync.RWMutex - sessions map[string][]Message -} - -func (h *InMemoryHistory) SaveMessage(ctx context.Context, sessionID string, msg Message) error { - h.mu.Lock() - defer h.mu.Unlock() - h.sessions[sessionID] = append(h.sessions[sessionID], msg) - return nil -} - -func (h *InMemoryHistory) GetMessages(ctx context.Context, sessionID string, limit int) ([]Message, error) { - h.mu.RLock() - defer h.mu.RUnlock() - msgs := h.sessions[sessionID] - if limit > 0 && len(msgs) > limit { - msgs = msgs[len(msgs)-limit:] - } - return msgs, nil +// MessageRepository 对话消息持久化契约。 +// PostgreSQL 实现:PgMessageRepository +// MemoryManager 通过 Write-Through 注入此接口。 +type MessageRepository interface { + SaveMessage(ctx context.Context, sessionID string, msg models.Message, tokensUsed int) error + GetMessages(ctx context.Context, sessionID string, limit int, beforeID int64) ([]StoredMessage, error) + GetLastMessage(ctx context.Context, sessionID string) (*StoredMessage, error) + GetMessageCount(ctx context.Context, sessionID string) (int, error) + GetSessionMessageStats(ctx context.Context, sessionIDs []string) (map[string]int, error) } ``` -注入点(应用启动时根据配置选择实现): +### SessionRepository ```go -func NewApp(cfg *Config) *App { - var history HistoryRepository - var usage UsageRepository - - switch cfg.Storage.Driver { - case "postgres": - pool, _ := pgxpool.New(ctx, cfg.Storage.DSN) - history = &PgHistory{pool: pool} - usage = &PgUsage{pool: pool} - default: // "memory" — MVP 默认 - history = &InMemoryHistory{sessions: make(map[string][]Message)} - usage = &InMemoryUsage{} - } - - return &App{ - orchestrator: NewOrchestrator(cfg.AI, history, usage), - sessionMgr: NewSessionManager(cfg.Session, history), - } +// SessionRepository 会话元数据持久化契约。 +// PostgreSQL 实现:PgSessionRepository +// MemoryManager 通过 Write-Through 注入此接口。 +type SessionRepository interface { + Save(ctx context.Context, session models.Session) error + FindByID(ctx context.Context, id string) (*models.Session, error) + FindByUser(ctx context.Context, userID string, page, size int) ([]models.Session, int, error) + UpdateTitle(ctx context.Context, id string, title string) error + UpdateConfig(ctx context.Context, id string, config models.SessionConfig) error + Touch(ctx context.Context, id string) error + Delete(ctx context.Context, id string) error } ``` -> 依赖倒置原则——业务层依赖接口,不依赖具体实现。MVP 注入 `InMemoryHistory`,上线时一行代码换成 `PgHistory`。 +### 注入方式 + +```go +// main.go 依赖注入 +if cfg.Storage.Driver == "postgres" { + pool, _ := store.NewPostgresPool(ctx, cfg.Storage.DSN) + userRepo = store.NewPgUserRepository(pool) + msgRepo = store.NewPgMessageRepository(pool) + sessRepo = store.NewPgSessionRepository(pool) + sessionMgr = session.NewMemoryManager(30*time.Minute, 20, + session.WithMessageRepository(msgRepo), + session.WithSessionRepository(sessRepo), + ) +} else { + userRepo = store.NewMemUserRepository() + sessionMgr = session.NewMemoryManager(30*time.Minute, 20) +} +``` + +> 依赖倒置原则——业务层依赖接口,不依赖具体实现。通过配置一行代码切换存储后端。 --- diff --git a/docs/04-技术选型.md b/docs/04-技术选型.md index 8875fac..95adc0b 100644 --- a/docs/04-技术选型.md +++ b/docs/04-技术选型.md @@ -4,23 +4,26 @@ 本文档记录项目中各项技术的**选型过程、替代方案对比和决策理由**。技术选型没有"绝对正确",只有"更适合"。 -**定位**:持久化部分是拓展选型,不阻塞 MVP(MVP 用内存存储即可)。前端边缘处理部分是 MVP 阶段就需要确定的技术栈。AI 服务栈(STT/LLM/TTS)已确定默认选型,可通过配置灵活切换。 +**定位**:本文档记录各项技术的选型过程和决策理由。AI 服务栈、持久化层、认证系统均已实现并通过配置灵活切换。前端边缘处理已确定技术栈。 ``` 技术选型 -├── AI 服务栈 +├── AI 服务栈(✅ 已实现) │ ├── STT: Deepgram(默认) / MiMo ASR │ ├── LLM: GPT-4o(默认) / 通义千问等 OpenAI 兼容模型 │ └── TTS: OpenAI TTS(默认) / MiMo TTS -├── 持久化层 → 数据库选型: PostgreSQL(规划中,MVP 阶段使用内存存储) -├── 认证与用户系统 +├── 持久化层(✅ 已实现) +│ ├── 数据库: PostgreSQL(pgx/v5,手写 SQL) +│ ├── 迁移: 嵌入式 SQL 文件,自动执行 +│ └── 存储模式: Memory(默认)+ Write-Through 到 PG / Redis(可切换) +├── 认证与用户系统(✅ 已实现) │ ├── 认证方案: JWT (HS256), access 15min + refresh 7day │ ├── JWT 库: golang-jwt/jwt/v5 │ ├── 密码哈希: bcrypt │ ├── 数据库驱动: pgx/v5(手写 SQL,不用 ORM) │ └── 前端 Token 存储: localStorage -└── 前端边缘处理层 - ├── 边缘推理: ONNX Runtime Web(规划中,MVP 使用 Canvas 像素比较) +└── 前端边缘处理层(✅ 已实现) + ├── 关键帧检测: Canvas 像素比较(160x120 降采样) ├── 语音检测: @ricky0123/vad-web └── 媒体采集: MediaDevices API ``` @@ -61,7 +64,7 @@ --- -## 二、持久化层选型(规划中,MVP 阶段使用内存存储) +## 二、持久化层选型(已实现) ### 数据特征分析 diff --git a/docs/06-语音交互.md b/docs/06-语音交互.md index 3ebc63d..9efb59d 100644 --- a/docs/06-语音交互.md +++ b/docs/06-语音交互.md @@ -56,7 +56,7 @@ vad.start(); 句子切分规则:按中文标点(`。!?`)、英文标点(`. ! ?`)和换行符切分。 -当前实现参数:Voice `"alloy"`、Speed `1.0`、OutputFmt `"mp3"`、SampleRate `24000`。 +当前实现参数:Voice `"mimo_default"`(可通过配置切换)、Speed `1.0`、OutputFmt `"mp3"`、SampleRate `24000`。 方案选择: - **OpenAI TTS**(默认):音质好,延迟中等,按字符计费,模型 tts-1 diff --git a/docs/11-持久化与用户系统设计.md b/docs/11-持久化与用户系统设计.md index 0cce137..3f5d2c7 100644 --- a/docs/11-持久化与用户系统设计.md +++ b/docs/11-持久化与用户系统设计.md @@ -709,42 +709,43 @@ storage: ## 八、实施阶段 -### Phase 1:用户认证系统 +### Phase 1:用户认证系统 ✅ -- [ ] 数据库 schema 迁移脚本(users, refresh_tokens 表) -- [ ] `internal/auth/` 包:TokenManager, bcrypt 工具, JWT 中间件 -- [ ] `internal/store/user.go`:UserRepository 接口 + PostgreSQL 实现 -- [ ] REST API:`/api/auth/register`, `/api/auth/login`, `/api/auth/refresh`, `/api/auth/logout` -- [ ] 单元测试 +- [x] 数据库 schema 迁移脚本(users, refresh_tokens 表)— `migrations/001_users.up.sql` +- [x] `internal/auth/` 包:TokenManager, bcrypt 工具, JWT 中间件 +- [x] `internal/store/user.go`:UserRepository 接口 + PostgreSQL 实现 + 内存实现 +- [x] REST API:`/api/auth/register`, `/api/auth/login`, `/api/auth/refresh`, `/api/auth/logout` +- [x] 单元测试 — `jwt_test.go`, `service_test.go`, `auth_test.go`, `user_test.go` -### Phase 2:对话 CRUD + 消息持久化 +### Phase 2:对话 CRUD + 消息持久化 ✅ -- [ ] 数据库 schema 迁移脚本(sessions, messages 表改造) -- [ ] `internal/store/conversation.go`:ConversationRepository 接口 + PostgreSQL 实现 -- [ ] Session Manager 扩展:Create 绑定 user_id, ListByUser, UpdateTitle -- [ ] REST API:`/api/conversations` CRUD + `/api/conversations/:id/messages` -- [ ] Write-through:AppendMessage 同时写 PostgreSQL +- [x] 数据库 schema 迁移脚本(sessions, messages 表)— `migrations/002_messages.up.sql`, `003_sessions.up.sql` +- [x] `internal/store/message.go`:MessageRepository 接口 + PostgreSQL 实现 +- [x] `internal/store/session.go`:SessionRepository 接口 + PostgreSQL 实现 +- [x] Session Manager 扩展:Create 绑定 user_id, ListByUser, UpdateTitle +- [x] REST API:`/api/conversations` CRUD + `/api/conversations/:id/messages` +- [x] Write-through:AppendMessage 同时写 PostgreSQL -### Phase 3:对话历史恢复 +### Phase 3:对话历史恢复 ✅ -- [ ] `sessionManager.LoadFromDB()` 实现 -- [ ] 对话标题自动生成逻辑 -- [ ] REST API:对话详情、历史消息查询(分页) +- [x] MemoryManager 支持从 PG 透明恢复会话(Get 时自动 LoadFromDB) +- [x] 对话标题自动生成逻辑(首条 user 消息前 20 字符) +- [x] REST API:对话详情、历史消息查询(游标分页) -### Phase 4:前端集成 +### Phase 4:前端集成 ✅ -- [ ] `useAuth` hook + 请求拦截器(自动附加 token、自动 refresh) -- [ ] `AuthPage` 组件(登录/注册表单) -- [ ] `ConversationList` 组件 -- [ ] `useConversations` hook -- [ ] 路由守卫:未登录重定向到 `/login` -- [ ] WebSocket 连接带 token + conversation_id -- [ ] `useVisionSession` 适配多对话切换 +- [x] `useAuth` hook + AuthProvider(自动附加 token、自动 refresh) +- [x] `AuthPage` 组件(登录/注册表单) +- [x] `SessionSidebar` 组件(对话列表、搜索、重命名、删除) +- [x] `useSessionList` hook(localStorage 持久化) +- [x] 路由守卫:未登录重定向到 AuthPage +- [x] WebSocket 连接带 token + conversation_id +- [x] `useVisionSession` 适配多对话切换 -### Phase 5:配置与收尾 +### Phase 5:配置与收尾 ✅ -- [ ] 配置结构体扩展(AuthConfig) -- [ ] config.yaml 更新 -- [ ] docker-compose 添加 PostgreSQL -- [ ] 集成测试 -- [ ] 更新 `02-系统架构.md` 和 `03-接口文档.md` +- [x] 配置结构体扩展(AuthConfig, SessionConfig, StorageConfig) +- [x] config.yaml 更新 +- [x] 数据库迁移嵌入式自动执行(`go:embed`) +- [x] 集成测试 — 122 个测试函数覆盖所有模块 +- [x] 更新 `02-系统架构.md` 和 `03-接口文档.md` diff --git a/docs/PLAN_BACKEND.md b/docs/PLAN_BACKEND.md index a069e09..52cacb2 100644 --- a/docs/PLAN_BACKEND.md +++ b/docs/PLAN_BACKEND.md @@ -1,5 +1,7 @@ # CamTalk 后端完善计划 +> **✅ 状态:全部完成。** 所有 Phase 已实现并通过测试(约 122 个测试函数)。本文档保留作为历史参考。 + ## Context 后端当前是一个骨架:`main.go` 启动 Gin 服务器,`ws/handler.go` 实现了 WebSocket 连接生命周期和消息分发,`models/models.go` 定义了所有协议消息类型,`config/config.go` 实现了 Viper 配置加载。但所有业务逻辑都是 TODO 桩——没有 Session Manager、没有 AI 服务客户端、没有编排层、没有日志/错误工具、没有测试。前端已基本完成,正在等待后端提供真实的 AI 管道。 @@ -10,7 +12,7 @@ ## 分阶段实施 -### Phase 1:基础设施(logger、errors、config 接入、graceful shutdown) +### Phase 1:基础设施(logger、errors、config 接入、graceful shutdown) ✅ **目标**:为后续模块提供日志、错误码、配置等基础能力,替换 `main.go` 中的硬编码值。 @@ -26,7 +28,7 @@ --- -### Phase 2:Session Manager +### Phase 2:Session Manager ✅ **目标**:实现会话生命周期管理,让 WS handler 能追踪会话、存储对话历史。 @@ -40,7 +42,7 @@ --- -### Phase 3:AI 服务层接口 + 实现 +### Phase 3:AI 服务层接口 + 实现 ✅ **目标**:定义并实现三个 AI 服务客户端,每个服务一个独立包。 @@ -62,7 +64,7 @@ --- -### Phase 4:AI Orchestrator(核心编排) +### Phase 4:AI Orchestrator(核心编排) ✅ **目标**:实现 STT → LLM → TTS 流式并行管道,这是后端最关键的业务逻辑。 @@ -78,7 +80,7 @@ --- -### Phase 5:WS Handler 完整接入 +### Phase 5:WS Handler 完整接入 ✅ **目标**:将 Session Manager + Orchestrator 串入 WebSocket handler,实现端到端消息处理。 @@ -92,7 +94,7 @@ --- -### Phase 6:REST API 补全 +### Phase 6:REST API 补全 ✅ **目标**:补全设计文档中的 REST 端点。 @@ -104,7 +106,7 @@ --- -### Phase 7:Rate Limiter + Model Router(可选/MVP 后) +### Phase 7:Rate Limiter + Model Router(可选/MVP 后) 📋 **目标**:防止滥用 + 智能模型选择,MVP 可简化或跳过。 @@ -116,7 +118,7 @@ --- -### Phase 8:集成测试 + 文档同步 +### Phase 8:集成测试 + 文档同步 ✅ | # | 任务 | 文件 | 说明 | |---|------|------|------| diff --git a/docs/PLAN_USER_MODULE.md b/docs/PLAN_USER_MODULE.md index 6e1dd9f..2d338ed 100644 --- a/docs/PLAN_USER_MODULE.md +++ b/docs/PLAN_USER_MODULE.md @@ -1,5 +1,7 @@ # CamTalk 后端用户模块构建计划 +> **✅ 状态:全部完成。** 所有 Phase 已实现并通过测试。本文档保留作为历史参考。 + ## Context 后端 AI 管道(STT → LLM → TTS)已完成,现在需要实现用户系统和对话持久化。目标:**用户注册登录后,可在对话列表中选择历史对话继续交谈**。 @@ -34,7 +36,7 @@ ## 分阶段实施 -### Phase 1:配置扩展 + 数据库连接 +### Phase 1:配置扩展 + 数据库连接 ✅ **目标**:扩展配置结构体,建立 PostgreSQL 连接池。 @@ -83,7 +85,7 @@ func NewPostgresPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) { --- -### Phase 2:用户模型 + Repository +### Phase 2:用户模型 + Repository ✅ **目标**:定义用户数据模型和持久化接口。 @@ -149,7 +151,7 @@ type User struct { --- -### Phase 3:JWT + 认证服务 +### Phase 3:JWT + 认证服务 ✅ **目标**:实现 JWT 签发/校验、bcrypt 密码处理、认证业务逻辑。 @@ -292,7 +294,7 @@ type Service interface { --- -### Phase 4:认证 REST API +### Phase 4:认证 REST API ✅ **目标**:实现注册、登录、刷新、登出四个端点。 @@ -350,7 +352,7 @@ const ( --- -### Phase 5:Session Manager 改造 +### Phase 5:Session Manager 改造 ✅ **目标**:Session Manager 关联 user_id,支持对话列表查询。 @@ -436,7 +438,7 @@ func (m *MemoryManager) ListByUser(ctx context.Context, userID string, page, siz --- -### Phase 6:对话 REST API +### Phase 6:对话 REST API ✅ **目标**:实现对话 CRUD 和历史消息查询端点。 @@ -493,7 +495,7 @@ func (h *ConversationHandler) getSessionForUser(c *gin.Context, sessionID string --- -### Phase 7:WebSocket 认证集成 +### Phase 7:WebSocket 认证集成 ✅ **目标**:WS 连接需要 JWT 认证,支持指定 conversation_id 恢复历史对话。 @@ -564,7 +566,7 @@ func generateTitle(firstMessage string) string { --- -### Phase 8:消息持久化(Write-Through) +### Phase 8:消息持久化(Write-Through) ✅ **目标**:对话消息同时写入 PostgreSQL,保证重启不丢数据。 @@ -643,7 +645,7 @@ func (m *MemoryManager) AppendMessage(ctx context.Context, sessionID string, msg --- -### Phase 9:旧端点废弃 + 集成收尾 +### Phase 9:旧端点废弃 + 集成收尾 ✅ **目标**:废弃旧的 `/api/sessions` 端点,完成全链路集成。 diff --git a/docs/README.md b/docs/README.md index 790d21d..ea96927 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,17 +4,21 @@ CamTalk 是一款多模态实时 AI 视觉对话助手。用户通过摄像头 ## 文档索引 -| 文档 | 说明 | -|------|------| -| [01-项目概述](01-项目概述.md) | 项目目标、核心挑战、交付物 | -| [02-系统架构](02-系统架构.md) | 三层架构、技术栈、核心交互流程、前后端模块、存储策略、部署架构 | -| [03-接口文档](03-接口文档.md) | WebSocket 协议、REST API、**AI 服务层接口**、**编排器设计**、**Session Manager**、**配置管理(Viper)**、数据模型、错误码、连接管理(**实现时首先阅读**) | -| [04-技术选型](04-技术选型.md) | 持久化层(PostgreSQL)和前端边缘处理层的选型对比与决策理由 | -| [05-用户故事](05-用户故事.md) | P0/P1/P2 用户故事、验收标准、优先级决策依据 | -| [06-语音交互](06-语音交互.md) | VAD → STT → LLM → TTS 全链路、延迟优化 | -| [07-视觉理解](07-视觉理解.md) | 帧采样策略、图像编码、多模态 LLM 输入机制 | -| [08-成本控制](08-成本控制.md) | 智能采样、端云协同、模型分级、缓存复用 | -| [09-技术名词解释](09-技术名词解释.md) | 前端/后端/AI 服务技术名词简明解释 | +| 文档 | 说明 | 状态 | +|------|------|------| +| [01-项目概述](01-项目概述.md) | 项目目标、核心挑战、交付物 | ✅ 与代码一致 | +| [02-系统架构](02-系统架构.md) | 三层架构、技术栈、核心交互流程、前后端模块、存储策略、部署架构 | ✅ 已更新 | +| [03-接口文档](03-接口文档.md) | WebSocket 协议、REST API、AI 服务层接口、编排器设计、Session Manager、配置管理(Viper)、数据模型、错误码、连接管理(**实现时首先阅读**) | ✅ 已更新 | +| [04-技术选型](04-技术选型.md) | 持久化层(PostgreSQL)、认证系统和前端边缘处理层的选型对比与决策理由 | ✅ 已更新 | +| [05-用户故事](05-用户故事.md) | P0/P1/P2 用户故事、验收标准、优先级决策依据 | ✅ 与代码一致 | +| [06-语音交互](06-语音交互.md) | VAD → STT → LLM → TTS 全链路、延迟优化 | ✅ 与代码一致 | +| [07-视觉理解](07-视觉理解.md) | 帧采样策略、图像编码、多模态 LLM 输入机制 | ✅ 与代码一致 | +| [08-成本控制](08-成本控制.md) | 智能采样、端云协同、模型分级、缓存复用 | ✅ 与代码一致 | +| [09-技术名词解释](09-技术名词解释.md) | 前端/后端/AI 服务技术名词简明解释 | ✅ 与代码一致 | +| [10-功能创意](10-功能创意.md) | 未来功能创意清单 | 📋 愿景 | +| [11-持久化与用户系统设计](11-持久化与用户系统设计.md) | 用户认证、JWT、对话持久化的完整设计方案 | ✅ 已全部实现 | +| [PLAN_BACKEND.md](PLAN_BACKEND.md) | 后端 AI 管道构建计划(Session Manager → AI 服务 → Orchestrator) | ✅ 已全部完成 | +| [PLAN_USER_MODULE.md](PLAN_USER_MODULE.md) | 后端用户模块构建计划(Auth → 对话 CRUD → 消息持久化) | ✅ 已全部完成 | ## 推荐阅读顺序 @@ -25,3 +29,18 @@ CamTalk 是一款多模态实时 AI 视觉对话助手。用户通过摄像头 5. **05-用户故事** — 明确功能优先级 6. **06~08** — 各技术领域的详细设计 7. **09-技术名词解释** — 遇到不熟悉的名词时查阅 +8. **11-持久化与用户系统设计** — 用户认证和持久化的详细设计 + +## 实现状态总览 + +前后端代码已全部实现,无 TODO/FIXME 桩代码。后端约 122 个测试函数覆盖所有模块。 + +| 层级 | 状态 | 说明 | +|------|------|------| +| 前端 | ✅ 已完成 | 10 个组件、3 个 Hook、10 个库模块、i18n 三语言 | +| 后端 AI 管道 | ✅ 已完成 | STT/LLM/TTS 多 provider、Orchestrator 流式并行 | +| 后端用户系统 | ✅ 已完成 | JWT 认证、用户注册登录、对话 CRUD、消息持久化 | +| 后端存储层 | ✅ 已完成 | Memory + PostgreSQL + Redis 三种实现 | +| 数据库迁移 | ✅ 已完成 | 3 个版本化迁移脚本,嵌入式自动执行 | +| Model Router | 📋 规划中 | 按问题复杂度选择模型 | +| Rate Limiter | 📋 规划中 | 令牌桶限流 |