Files
note/课题一/AI 视觉对话助手/项目实现/接口文档.md
2026-06-12 15:55:55 +08:00

608 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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
```
## 关联笔记
- [[项目架构与技术栈]]
- [[技术选型]]
- [[项目架构与技术栈/技术名词解释]]