--- tags: [API, WebSocket, 接口设计, MVP, Go, TypeScript, 扩展性] create time: 2026-06-12 15:41 --- # 接口文档 ## 概述 本文档定义 AI 视觉对话助手的**前后端通信接口**。以 MVP 为核心目标:用 WebSocket 承载实时对话,用最少的 REST 端点支撑基础运维。**暂不实现持久化**,但通过 Repository 接口模式为后续扩展(对话历史、用量统计)预留干净的接入点。 > [!info] 设计原则 > - **WebSocket 为主**:实时对话是核心场景,所有对话数据走 WebSocket > - **REST 为辅**:仅用于健康检查、会话管理等低频操作 > - **接口先行**:先定义契约,再填充实现——前后端可并行开发 ## 正文 ### 接口全景 ```mermaid graph LR subgraph Client["浏览器"] WS_C["WebSocket Client"] HTTP_C["HTTP Client"] end subgraph Server["Go Gateway :8080"] WS_EP["/ws"] HEALTH_EP["/api/health"] SESSION_EP["/api/sessions"] end WS_C <-->|"实时对话"| WS_EP HTTP_C -->|"GET"| HEALTH_EP HTTP_C <-->|"POST / DELETE"| SESSION_EP ``` ### 一、WebSocket 协议 连接地址:`ws://localhost:8080/ws` #### 1.1 连接生命周期 ```mermaid sequenceDiagram participant C as Client participant S as Server C->>S: WebSocket Upgrade 请求 S-->>C: 101 Switching Protocols S-->>C: {"type":"connected","session_id":"..."} Note over C,S: 连接建立,进入对话 C->>S: {"type":"query",...} S-->>C: {"type":"stt_result",...} S-->>C: {"type":"llm_chunk",...} S-->>C: {"type":"llm_chunk",...} S-->>C: {"type":"llm_done",...} S-->>C: {"type":"tts_audio",...} C->>S: {"type":"query",...} Note over C,S: 持续对话... C->>S: {"type":"ping"} S-->>C: {"type":"pong"} C->>S: WebSocket Close S-->>C: WebSocket Close Ack ``` #### 1.2 消息格式约定 所有 WebSocket 消息均为 **JSON 文本帧**,统一结构: ```typescript // 通用消息信封 interface WsMessage { type: string; // 消息类型,必填 request_id?: string; // 可选,用于请求-响应关联 timestamp?: number; // 可选,毫秒时间戳 [key: string]: any; // 类型特定字段 } ``` #### 1.3 客户端 → 服务端消息 ##### `query` —— 发起一次视觉对话 用户说完话后,客户端同时发送当前图像帧和语音片段: ```typescript interface QueryMessage { type: "query"; request_id: string; // 客户端生成的 UUID image: string; // Base64 编码的 JPEG 图像(不含 data: 前缀) audio: string; // Base64 编码的音频片段(PCM 16kHz) mime_type?: string; // 音频格式,默认 "audio/pcm" } ``` > [!question] 思考 > 为什么图像和音频放在同一条消息里?因为 VAD 检测到用户说完话时,需要同时捕获"此刻的画面"和"说的话",拆成两条消息会增加时序同步的复杂度。 ##### `config` —— 更新会话配置 运行时调整 AI 行为参数,无需重建连接: ```typescript interface ConfigMessage { type: "config"; payload: { tts_enabled?: boolean; // 是否开启语音合成,默认 true detail_level?: "low" | "high"; // 图像精度,默认 "low" language?: string; // 交互语言,默认 "zh-CN" }; } ``` ##### `interrupt` —— 打断当前回复 用户在 AI 回复过程中再次说话,打断正在进行的 LLM/TTS 流: ```typescript interface InterruptMessage { type: "interrupt"; request_id?: string; // 可选,指定打断哪次请求 } ``` ##### `ping` —— 心跳保活 ```typescript interface PingMessage { type: "ping"; } ``` #### 1.4 服务端 → 客户端消息 ##### `connected` —— 连接建立确认 ```typescript interface ConnectedMessage { type: "connected"; session_id: string; // 服务端生成的会话 ID server_version: string; // 服务端版本号,如 "0.1.0" } ``` ##### `stt_result` —— 语音识别结果 LLM 推理前,先返回 STT 识别出的文本,让用户看到"我听到了什么": ```typescript interface STTResultMessage { type: "stt_result"; request_id: string; text: string; // 识别出的用户语音文本 is_final: boolean; // 是否为最终结果(流式场景下可能分多段) } ``` ##### `llm_chunk` —— LLM 流式输出片段 ```typescript interface LLMChunkMessage { type: "llm_chunk"; request_id: string; delta: string; // 本次增量文本 role: "assistant"; } ``` ##### `llm_done` —— LLM 输出完成 ```typescript interface LLMDoneMessage { type: "llm_done"; request_id: string; full_text: string; // 完整回复文本 tokens_used: { prompt: number; // 输入 token 数 completion: number; // 输出 token 数 total: number; }; model: string; // 实际使用的模型名 latency_ms: number; // 端到端延迟(毫秒) } ``` ##### `tts_audio` —— TTS 音频流片段 ```typescript interface TTSAudioMessage { type: "tts_audio"; request_id: string; audio: string; // Base64 编码的音频片段 mime_type: string; // "audio/mp3" 或 "audio/pcm" is_last: boolean; // 是否为最后一片 } ``` ##### `error` —— 错误通知 ```typescript interface ErrorMessage { type: "error"; request_id?: string; // 关联的请求(可选) code: string; // 错误码,见下方错误码表 message: string; // 人类可读的错误描述 } ``` ##### `pong` —— 心跳响应 ```typescript interface PongMessage { type: "pong"; } ``` #### 1.5 消息流时序总览 一次完整交互的消息流转: ```mermaid sequenceDiagram participant C as Client participant S as Server Note over C: VAD 检测到语音结束 C->>S: query {image, audio} S-->>C: stt_result {text, is_final: true} loop LLM 流式输出 S-->>C: llm_chunk {delta: "这"} S-->>C: llm_chunk {delta: "是一"} S-->>C: llm_chunk {delta: "朵花..."} end S-->>C: llm_done {full_text, tokens_used, latency_ms} loop TTS 音频流 S-->>C: tts_audio {audio, is_last: false} S-->>C: tts_audio {audio, is_last: true} end ``` ### 二、REST API MVP 阶段仅暴露最少量的 HTTP 端点: #### 2.1 健康检查 ```http GET /api/health ``` 响应: ```json { "status": "ok", "version": "0.1.0", "uptime_seconds": 3600, "active_sessions": 42 } ``` #### 2.2 创建会话(可选) MVP 阶段 WebSocket 连接即自动创建会话,此端点为**预留扩展**: ```http POST /api/sessions Content-Type: application/json { "user_id": "optional-user-id", "config": { "tts_enabled": true, "detail_level": "low", "language": "zh-CN" } } ``` 响应: ```json { "session_id": "550e8400-e29b-41d4-a716-446655440000", "created_at": "2026-06-12T15:41:00Z" } ``` #### 2.3 销毁会话 ```http DELETE /api/sessions/{session_id} ``` 响应:`204 No Content` #### 2.4 预留端点(暂不实现) > [!info] 后续扩展 > 引入持久化后,按需添加以下端点: | 端点 | 方法 | 用途 | MVP 状态 | |------|------|------|---------| | `/api/sessions/{id}/messages` | GET | 查询对话历史 | 预留,暂不实现 | | `/api/usage` | GET | 查询用量统计 | 预留,暂不实现 | | `/api/users/{id}/preferences` | GET/PUT | 用户偏好管理 | 预留,暂不实现 | ### 三、数据模型 #### 3.1 Go 后端模型 ```go // ---- 核心模型(MVP 实现)---- type Session struct { ID string `json:"session_id"` CreatedAt time.Time `json:"created_at"` Config SessionConfig `json:"config"` } type SessionConfig struct { TTSEnabled bool `json:"tts_enabled"` DetailLevel string `json:"detail_level"` // "low" | "high" Language string `json:"language"` } type QueryRequest struct { RequestID string `json:"request_id"` Image []byte `json:"-"` // Base64 解码后 Audio []byte `json:"-"` // Base64 解码后 MimeType string `json:"mime_type"` } type Message struct { Role string `json:"role"` // "user" | "assistant" Content string `json:"content"` ImageURL string `json:"image_url,omitempty"` TokensUsed int `json:"tokens_used,omitempty"` } ``` ```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) } type UsageRecord struct { SessionID string `json:"session_id"` LLMTokens int `json:"llm_tokens"` STTSeconds float64 `json:"stt_seconds"` TTSChars int `json:"tts_chars"` EstimatedCost float64 `json:"estimated_cost"` } type UsageDaily struct { Date string `json:"date"` LLMTokens int `json:"llm_tokens"` EstimatedCost float64 `json:"estimated_cost"` } ``` #### 3.2 TypeScript 前端模型 ```typescript // ---- 核心模型 ---- interface Session { sessionId: string; createdAt: string; config: SessionConfig; } interface SessionConfig { ttsEnabled: boolean; detailLevel: "low" | "high"; language: string; } interface ChatMessage { role: "user" | "assistant"; content: string; imageUrl?: string; // 关键帧(用户消息可附带) timestamp: number; tokensUsed?: number; // 仅 assistant 消息 } // ---- WebSocket 消息联合类型 ---- type ServerMessage = | ConnectedMessage | STTResultMessage | LLMChunkMessage | LLMDoneMessage | TTSAudioMessage | ErrorMessage | PongMessage; type ClientMessage = | QueryMessage | ConfigMessage | InterruptMessage | PingMessage; ``` ### 四、扩展接口设计 通过**接口(Interface)模式**隔离存储层,MVP 用内存实现,后续替换为数据库实现——业务逻辑层零改动。 #### 4.1 Repository 接口 ```mermaid graph TD subgraph Biz["业务逻辑层(不变)"] ORCH["AI Orchestrator"] SM["Session Manager"] end subgraph Repo["存储接口层"] HR["HistoryRepository"] UR["UsageRepository"] end subgraph Impl_MVP["MVP 实现"] MEM_H["InMemoryHistory"] MEM_U["InMemoryUsage"] end subgraph Impl_Future["后续实现"] PG_H["PgHistory"] PG_U["PgUsage"] end ORCH --> HR ORCH --> UR SM --> HR HR --> MEM_H UR --> MEM_U HR -.->|"替换"| PG_H UR -.->|"替换"| PG_U ``` #### 4.2 MVP 内存实现 ```go // InMemoryHistory —— MVP 阶段的对话历史实现 // 数据存在内存 map 中,连接断开即丢 type InMemoryHistory struct { mu sync.RWMutex sessions map[string][]Message // sessionID -> messages } 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 } ``` #### 4.3 后续替换为 PostgreSQL 引入持久化时,只需新增一个实现,无需修改业务逻辑: ```go // PgHistory —— PostgreSQL 实现(后续扩展) type PgHistory struct { pool *pgxpool.Pool } func (p *PgHistory) SaveMessage(ctx context.Context, sessionID string, msg Message) error { _, err := p.pool.Exec(ctx, `INSERT INTO messages (session_id, role, content, image_url, tokens_used) VALUES ($1, $2, $3, $4, $5)`, sessionID, msg.Role, msg.Content, msg.ImageURL, msg.TokensUsed, ) return err } func (p *PgHistory) GetMessages(ctx context.Context, sessionID string, limit int) ([]Message, error) { rows, _ := p.pool.Query(ctx, `SELECT role, content, image_url, tokens_used FROM messages WHERE session_id = $1 ORDER BY created_at DESC LIMIT $2`, sessionID, limit, ) defer rows.Close() // ... scan and return } ``` > [!question] 思考 > 这就是**依赖倒置原则**——业务层依赖接口(`HistoryRepository`),不依赖具体实现。MVP 阶段注入 `InMemoryHistory`,上线时一行代码换成 `PgHistory`,其余逻辑完全不动。 #### 4.4 注入点示例 在应用启动时根据配置选择实现: ```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), } } ``` ### 五、错误码定义 | 错误码 | 含义 | 客户端处理建议 | |--------|------|--------------| | `INVALID_MESSAGE` | 消息格式不合法 | 检查 JSON 结构,不重试 | | `SESSION_NOT_FOUND` | 会话不存在或已过期 | 重新建立 WebSocket 连接 | | `RATE_LIMITED` | 请求频率超限 | 延迟后重试,提示用户稍等 | | `IMAGE_TOO_LARGE` | 图像超过 4MB 限制 | 降低分辨率或压缩质量 | | `AUDIO_TOO_SHORT` | 音频片段 < 250ms | 忽略,等待下次语音输入 | | `LLM_TIMEOUT` | LLM 推理超时(>10s) | 提示用户重试 | | `LLM_ERROR` | LLM 服务异常 | 提示用户重试,服务端记录日志 | | `STT_ERROR` | 语音识别失败 | 回退到纯文本输入模式 | | `TTS_ERROR` | 语音合成失败 | 静默回退到纯文本回复 | | `INTERNAL_ERROR` | 服务端内部错误 | 提示用户重试 | > [!tip] 错误处理原则 > 客户端收到 `error` 消息后,应根据错误码分别处理:可恢复的(如 `RATE_LIMITED`)自动重试;不可恢复的(如 `IMAGE_TOO_LARGE`)提示用户调整;服务端异常(如 `INTERNAL_ERROR`)记录日志并提示重试。 ### 六、连接管理 #### 心跳机制 ```mermaid sequenceDiagram participant C as Client participant S as Server loop 每 30 秒 C->>S: ping S-->>C: pong end Note over S: 超过 60 秒无 ping S->>S: 判定连接断开 S->>S: 清理会话资源 ``` #### 重连策略 客户端断线后按**指数退避**重连: ```typescript function reconnect(attempt: number) { const delay = Math.min(1000 * Math.pow(2, attempt), 30000); // 最大 30s const jitter = Math.random() * 1000; // 随机抖动 setTimeout(() => connect(), delay + jitter); } // attempt: 0 → 1s, 1 → 2s, 2 → 4s, 3 → 8s, ... 最大 30s ``` ## 关联笔记 - [[项目架构与技术栈]] - [[技术选型]] - [[项目架构与技术栈/技术名词解释]]