docs: 同步文档与代码实现状态

- 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: 新增实现状态总览,补充文档索引
This commit is contained in:
hhs
2026-06-19 14:58:43 +08:00
parent 16302af7d2
commit dca37f3e48
8 changed files with 333 additions and 213 deletions

View File

@@ -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 标记当前正在处理的请求 IDinterrupt 用)。
@@ -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)
}
```
> 依赖倒置原则——业务层依赖接口,不依赖具体实现。通过配置一行代码切换存储后端。
---