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:
261
docs/03-接口文档.md
261
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)
|
||||
}
|
||||
```
|
||||
|
||||
> 依赖倒置原则——业务层依赖接口,不依赖具体实现。通过配置一行代码切换存储后端。
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user