API文档.md 25 KB

API文档

文档说明

本文档定义前端浏览器与后端WSS服务之间的通信协议和接口规范。


1. 概述

1.1 通信架构

┌─────────────┐         WSS/HTTPS        ┌─────────────┐        WebSocket       ┌─────────────┐
│             │ ◄────────────────────── │             │ ◄─────────────────── │             │
│   浏览器     │                          │  WSS服务器   │                       │  豆包API     │
│  (前端)     │ ─────────────────────► │  (后端)     │ ──────────────────► │             │
└─────────────┘    实时音频流/控制消息    └─────────────┘   实时音频流/控制消息   └─────────────┘

1.2 核心特性

  • 协议: WebSocket Secure (WSS)
  • 传输模式: 全双工、实时流式传输
  • 音频格式:
    • 上行: PCM (16kHz, 16bit, 单声道, 小端序)
    • 下行: OGG/Opus
  • 消息类型: 二进制(音频)+ JSON(控制)

1.3 连接要求

要求项 说明
协议 WSS (WebSocket over TLS)
端口 443 (生产环境) / 8443 (开发环境)
认证 Token/API Key (可选)
心跳 每30秒一次ping/pong
超时 60秒无数据自动断开

2. WebSocket连接

2.1 连接端点

生产环境:

wss://api.yourdomain.com/ws/realtime

开发环境:

wss://localhost:8443/ws/realtime

2.2 连接建立

请求头(可选认证)

GET /ws/realtime HTTP/1.1
Host: api.yourdomain.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Authorization: Bearer <your-token>  # 可选

连接响应

成功 (101 Switching Protocols):

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

失败响应:

状态码 说明 处理建议
400 请求格式错误 检查请求头
401 认证失败 检查Token
403 权限不足 联系管理员
429 请求过于频繁 稍后重试
503 服务不可用 等待服务恢复

2.3 连接流程

// 前端连接示例
class RealtimeClient {
  private ws: WebSocket | null = null;
  
  connect(token?: string) {
    const url = 'wss://api.yourdomain.com/ws/realtime';
    const fullUrl = token ? `${url}?token=${token}` : url;
    
    this.ws = new WebSocket(fullUrl);
    this.ws.binaryType = 'arraybuffer';
    
    this.ws.onopen = () => {
      console.log('WebSocket连接成功');
      // 发送初始化消息
      this.sendMessage({
        type: 'init',
        payload: {
          version: '1.0',
          capabilities: ['pcm', 'opus']
        }
      });
    };
    
    this.ws.onmessage = (event) => {
      this.handleMessage(event);
    };
    
    this.ws.onerror = (error) => {
      console.error('WebSocket错误:', error);
    };
    
    this.ws.onclose = (event) => {
      console.log('WebSocket关闭:', event.code, event.reason);
      this.handleReconnect();
    };
  }
}

3. 消息协议

3.1 消息类型

WebSocket支持两种消息类型:

类型 用途 格式
文本消息 控制指令、状态同步 JSON
二进制消息 音频数据传输 ArrayBuffer

3.2 文本消息格式(JSON)

基础结构:

interface ControlMessage {
  type: string;           // 消息类型
  id?: string;           // 消息ID(用于追踪)
  timestamp?: number;    // 时间戳(毫秒)
  payload?: any;         // 消息负载
}

3.3 二进制消息格式

音频数据帧:

[Header: 4 bytes] [Audio Data: N bytes]

Header结构:
- Byte 0: 帧类型 (0x01=PCM, 0x02=Opus)
- Byte 1-3: 数据长度(小端序)

或简化版(推荐):

[Audio Data: N bytes]  // 直接发送原始音频数据

4. 控制消息定义

4.1 客户端 → 服务端

4.1.1 初始化连接

消息类型: init

{
  "type": "init",
  "id": "msg_001",
  "timestamp": 1696752000000,
  "payload": {
    "version": "1.0",
    "client": "web",
    "capabilities": ["pcm", "opus"],
    "settings": {
      "sampleRate": 16000,
      "channels": 1,
      "bitDepth": 16
    }
  }
}

响应:

{
  "type": "init_ack",
  "id": "msg_001",
  "timestamp": 1696752000100,
  "payload": {
    "status": "success",
    "sessionId": "session_abc123",
    "config": {
      "maxAudioChunkSize": 8192,
      "heartbeatInterval": 30000
    }
  }
}

4.1.2 开始录音

消息类型: start_recording

{
  "type": "start_recording",
  "id": "msg_002",
  "timestamp": 1696752010000,
  "payload": {
    "sessionId": "session_abc123"
  }
}

响应:

{
  "type": "recording_started",
  "id": "msg_002",
  "timestamp": 1696752010050,
  "payload": {
    "status": "ready",
    "message": "准备接收音频数据"
  }
}

4.1.3 停止录音

消息类型: stop_recording

{
  "type": "stop_recording",
  "id": "msg_003",
  "timestamp": 1696752015000,
  "payload": {
    "sessionId": "session_abc123",
    "reason": "user_released"  // user_released | timeout | error
  }
}

响应:

{
  "type": "recording_stopped",
  "id": "msg_003",
  "timestamp": 1696752015050,
  "payload": {
    "status": "success",
    "duration": 5000,  // 录音时长(毫秒)
    "message": "录音已停止,等待AI回复"
  }
}

4.1.4 心跳

消息类型: ping

{
  "type": "ping",
  "timestamp": 1696752020000
}

响应:

{
  "type": "pong",
  "timestamp": 1696752020010
}

4.1.5 关闭连接

消息类型: close

{
  "type": "close",
  "payload": {
    "reason": "user_exit"
  }
}

4.2 服务端 → 客户端

4.2.1 AI回复开始

消息类型: ai_response_start

{
  "type": "ai_response_start",
  "timestamp": 1696752015200,
  "payload": {
    "responseId": "resp_xyz789",
    "estimatedDuration": 3000,  // 预估时长(可选)
    "message": "AI开始回复"
  }
}

4.2.2 AI回复结束

消息类型: ai_response_end

{
  "type": "ai_response_end",
  "timestamp": 1696752018500,
  "payload": {
    "responseId": "resp_xyz789",
    "duration": 3300,
    "audioSize": 52800,  // 音频数据大小(字节)
    "message": "AI回复完成"
  }
}

4.2.3 状态变更通知

消息类型: state_change

{
  "type": "state_change",
  "timestamp": 1696752010000,
  "payload": {
    "state": "listening",  // idle | listening | speaking
    "previousState": "idle",
    "reason": "user_started_recording"
  }
}

4.2.4 错误通知

消息类型: error

{
  "type": "error",
  "timestamp": 1696752010000,
  "payload": {
    "code": "CONNECTION_POOL_EXHAUSTED",
    "message": "连接池已耗尽,请稍后重试",
    "severity": "error",  // warning | error | critical
    "recoverable": true
  }
}

4.2.5 性能指标

消息类型: metrics

{
  "type": "metrics",
  "timestamp": 1696752018500,
  "payload": {
    "latency": {
      "total": 285,        // 总延迟(毫秒)
      "network": 45,       // 网络延迟
      "processing": 150,   // 处理延迟
      "ai": 90            // AI延迟
    },
    "audio": {
      "received": 80000,   // 接收字节数
      "sent": 52800       // 发送字节数
    }
  }
}

5. 音频数据传输

5.1 上行音频(客户端 → 服务端)

格式要求:

  • 编码: PCM (未压缩)
  • 采样率: 16000 Hz
  • 位深: 16 bit (Int16)
  • 声道: 单声道 (Mono)
  • 字节序: 小端序 (Little Endian)
  • 分片大小: 512-2048 字节

发送示例:

// 发送PCM音频数据
sendAudioChunk(pcmData: Int16Array) {
  if (this.ws?.readyState === WebSocket.OPEN) {
    this.ws.send(pcmData.buffer);
  }
}

// 来自Recorder的实时数据
recorder = Recorder({
  type: 'pcm',
  sampleRate: 16000,
  bitRate: 16,
  onProcess: (buffers, powerLevel, duration, sampleRate) => {
    const pcmData = Recorder.SampleData(buffers, sampleRate, 16000);
    const int16Data = new Int16Array(pcmData.data);
    this.sendAudioChunk(int16Data);
  }
});

5.2 下行音频(服务端 → 客户端)

格式:

  • 编码: OGG 封装的 Opus 音频
  • 特点: 压缩率高,传输效率高
  • 浏览器支持: 现代浏览器原生支持

接收示例:

// 接收音频数据
handleMessage(event: MessageEvent) {
  if (event.data instanceof ArrayBuffer) {
    // 二进制数据 = 音频
    this.appendAudioChunk(event.data);
  } else {
    // 文本数据 = 控制消息
    const message = JSON.parse(event.data);
    this.handleControlMessage(message);
  }
}

// 使用MediaSource API流式播放
appendAudioChunk(chunk: ArrayBuffer) {
  if (!this.sourceBuffer.updating) {
    this.sourceBuffer.appendBuffer(chunk);
  } else {
    this.audioQueue.push(chunk);
  }
  
  // 自动开始播放
  if (this.audio.paused && this.sourceBuffer.buffered.length > 0) {
    this.audio.play();
  }
}

6. 完整交互流程

6.1 初始化流程

sequenceDiagram
    participant C as 客户端
    participant S as WSS服务器
    participant D as 豆包API连接池
    
    C->>S: WebSocket连接请求
    S->>S: 验证请求
    S->>C: 101 连接成功
    
    C->>S: init (初始化消息)
    S->>D: 从连接池获取连接
    D-->>S: 返回可用连接
    S->>C: init_ack (会话ID)
    
    Note over C,S: 连接建立完成

6.2 对话流程

sequenceDiagram
    participant C as 客户端
    participant S as WSS服务器
    participant D as 豆包API
    
    Note over C: 用户按下按钮
    C->>S: start_recording
    S->>C: recording_started
    S->>C: state_change (listening)
    
    loop 实时流式传输
        C->>S: PCM音频块
        S->>D: 转发PCM音频块
    end
    
    Note over C: 用户松开按钮
    C->>S: stop_recording
    S->>C: recording_stopped
    S->>D: 停止音频输入
    
    S->>C: ai_response_start
    S->>C: state_change (speaking)
    
    loop 实时流式接收
        D->>S: OGG/Opus音频块
        S->>C: 转发音频块
        C->>C: 边接收边播放
    end
    
    S->>C: ai_response_end
    S->>C: state_change (idle)
    S->>C: metrics (性能指标)

6.3 错误处理流程

sequenceDiagram
    participant C as 客户端
    participant S as WSS服务器
    
    C->>S: 音频数据
    S->>S: 检测到错误
    S->>C: error (错误通知)
    
    alt 可恢复错误
        C->>C: 显示提示
        C->>S: 重试操作
    else 不可恢复错误
        C->>C: 显示错误
        C->>S: close
        S->>C: WebSocket关闭
    end

7. 错误码定义

7.1 客户端错误 (4xxx)

错误码 名称 说明 可恢复
4000 INVALID_MESSAGE 消息格式错误 是
4001 INVALID_AUDIO_FORMAT 音频格式不支持 否
4002 AUDIO_TOO_LARGE 音频数据过大 是
4003 RATE_LIMIT_EXCEEDED 请求频率超限 是
4004 SESSION_EXPIRED 会话已过期 否
4005 UNAUTHORIZED 未授权 否

7.2 服务端错误 (5xxx)

错误码 名称 说明 可恢复
5000 INTERNAL_ERROR 内部服务器错误 是
5001 CONNECTION_POOL_EXHAUSTED 连接池已耗尽 是
5002 DOUBAO_API_ERROR 豆包API错误 是
5003 DOUBAO_API_TIMEOUT 豆包API超时 是
5004 SERVICE_UNAVAILABLE 服务不可用 是

7.3 错误消息示例

{
  "type": "error",
  "timestamp": 1696752010000,
  "payload": {
    "code": "CONNECTION_POOL_EXHAUSTED",
    "message": "服务繁忙,连接池已满,请稍后重试",
    "severity": "error",
    "recoverable": true,
    "retryAfter": 3000,  // 建议3秒后重试
    "details": {
      "poolSize": 10,
      "activeConnections": 10
    }
  }
}

8. 状态机定义

8.1 连接状态

状态 值 说明
CONNECTING 0 连接中
OPEN 1 已连接
CLOSING 2 关闭中
CLOSED 3 已关闭

8.2 会话状态

状态 说明 可触发操作
IDLE 空闲状态 开始录音
LISTENING 倾听状态(用户说话) 停止录音
PROCESSING 处理中(等待AI) 无
SPEAKING 说话状态(AI回复) 无

状态转换:

IDLE → (用户按下) → LISTENING
LISTENING → (用户松开) → PROCESSING
PROCESSING → (AI开始回复) → SPEAKING
SPEAKING → (播放完成) → IDLE

9. 性能指标

9.1 延迟指标

指标 目标值 说明
连接建立延迟 < 100ms WebSocket握手时间
首包延迟 < 50ms 收到第一个音频包的时间
音频传输延迟 < 20ms 单个音频包传输时间
端到端延迟 < 300ms 用户停止说话到听到回复

9.2 带宽估算

上行带宽:

PCM音频: 16000 Hz × 16 bit × 1 channel = 256 kbps = 32 KB/s

下行带宽:

Opus音频: 约 24-32 kbps (压缩后) = 3-4 KB/s

10. 安全性

10.1 传输安全

  • ✅ 强制WSS: 所有连接必须通过TLS加密
  • ✅ 证书验证: 验证服务器证书有效性
  • ✅ Token认证: 可选的Bearer Token认证

10.2 数据安全

  • ✅ 音频数据: 仅在内存中处理,不持久化
  • ✅ 敏感信息: 不在日志中记录音频内容
  • ✅ 会话隔离: 每个会话独立处理

10.3 速率限制

限制类型 限制值 说明
连接频率 10次/分钟 每IP每分钟最多建立10个连接
消息频率 100条/秒 每连接每秒最多100条控制消息
音频数据 1MB/秒 每连接每秒最多1MB音频数据

11. 客户端SDK示例

11.1 完整实现

interface RealtimeClientOptions {
  url: string;
  token?: string;
  autoReconnect?: boolean;
  maxReconnectAttempts?: number;
}

class RealtimeClient {
  private ws: WebSocket | null = null;
  private sessionId: string = '';
  private reconnectAttempts: number = 0;
  private options: RealtimeClientOptions;
  
  // 事件回调
  public onOpen?: () => void;
  public onClose?: (code: number, reason: string) => void;
  public onError?: (error: Error) => void;
  public onAudioData?: (data: ArrayBuffer) => void;
  public onStateChange?: (state: string) => void;
  public onMetrics?: (metrics: any) => void;
  
  constructor(options: RealtimeClientOptions) {
    this.options = {
      autoReconnect: true,
      maxReconnectAttempts: 5,
      ...options
    };
  }
  
  // 连接
  connect(): Promise<void> {
    return new Promise((resolve, reject) => {
      const url = this.options.token 
        ? `${this.options.url}?token=${this.options.token}`
        : this.options.url;
      
      this.ws = new WebSocket(url);
      this.ws.binaryType = 'arraybuffer';
      
      this.ws.onopen = () => {
        console.log('WebSocket已连接');
        this.reconnectAttempts = 0;
        
        // 发送初始化消息
        this.sendControlMessage({
          type: 'init',
          payload: {
            version: '1.0',
            client: 'web',
            capabilities: ['pcm', 'opus']
          }
        });
        
        this.onOpen?.();
        resolve();
      };
      
      this.ws.onmessage = (event) => {
        if (event.data instanceof ArrayBuffer) {
          // 音频数据
          this.onAudioData?.(event.data);
        } else {
          // 控制消息
          const message = JSON.parse(event.data);
          this.handleControlMessage(message);
        }
      };
      
      this.ws.onerror = (error) => {
        console.error('WebSocket错误:', error);
        this.onError?.(new Error('WebSocket错误'));
        reject(error);
      };
      
      this.ws.onclose = (event) => {
        console.log('WebSocket关闭:', event.code, event.reason);
        this.onClose?.(event.code, event.reason);
        
        if (this.options.autoReconnect && !event.wasClean) {
          this.handleReconnect();
        }
      };
    });
  }
  
  // 发送控制消息
  private sendControlMessage(message: any) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({
        ...message,
        timestamp: Date.now()
      }));
    }
  }
  
  // 处理控制消息
  private handleControlMessage(message: any) {
    switch (message.type) {
      case 'init_ack':
        this.sessionId = message.payload.sessionId;
        console.log('会话已建立:', this.sessionId);
        break;
        
      case 'state_change':
        this.onStateChange?.(message.payload.state);
        break;
        
      case 'metrics':
        this.onMetrics?.(message.payload);
        break;
        
      case 'error':
        console.error('服务器错误:', message.payload);
        this.onError?.(new Error(message.payload.message));
        break;
    }
  }
  
  // 开始录音
  startRecording() {
    this.sendControlMessage({
      type: 'start_recording',
      payload: { sessionId: this.sessionId }
    });
  }
  
  // 停止录音
  stopRecording() {
    this.sendControlMessage({
      type: 'stop_recording',
      payload: { 
        sessionId: this.sessionId,
        reason: 'user_released'
      }
    });
  }
  
  // 发送音频数据
  sendAudio(audioData: ArrayBuffer) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(audioData);
    }
  }
  
  // 关闭连接
  close() {
    this.sendControlMessage({
      type: 'close',
      payload: { reason: 'user_exit' }
    });
    
    setTimeout(() => {
      this.ws?.close();
    }, 100);
  }
  
  // 重连处理
  private handleReconnect() {
    if (this.reconnectAttempts >= (this.options.maxReconnectAttempts || 5)) {
      console.error('重连次数已达上限');
      return;
    }
    
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
    console.log(`${delay}ms后尝试重连...`);
    
    setTimeout(() => {
      this.reconnectAttempts++;
      this.connect().catch(console.error);
    }, delay);
  }
}

// 使用示例
const client = new RealtimeClient({
  url: 'wss://api.yourdomain.com/ws/realtime',
  token: 'your-auth-token'
});

client.onAudioData = (data) => {
  // 播放音频
  audioPlayer.appendChunk(data);
};

client.onStateChange = (state) => {
  // 更新UI状态
  updateAnimationState(state);
};

await client.connect();
client.startRecording();

12. 服务端实现示例

12.1 FastAPI实现

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import Dict
import asyncio
import json
import time

app = FastAPI()

class ConnectionManager:
    def __init__(self):
        self.active_connections: Dict[str, WebSocket] = {}
        self.sessions: Dict[str, dict] = {}
    
    async def connect(self, websocket: WebSocket, session_id: str):
        await websocket.accept()
        self.active_connections[session_id] = websocket
        self.sessions[session_id] = {
            'state': 'idle',
            'connected_at': time.time()
        }
    
    def disconnect(self, session_id: str):
        if session_id in self.active_connections:
            del self.active_connections[session_id]
        if session_id in self.sessions:
            del self.sessions[session_id]
    
    async def send_message(self, session_id: str, message: dict):
        if session_id in self.active_connections:
            await self.active_connections[session_id].send_text(
                json.dumps(message)
            )
    
    async def send_audio(self, session_id: str, audio_data: bytes):
        if session_id in self.active_connections:
            await self.active_connections[session_id].send_bytes(audio_data)

manager = ConnectionManager()

@app.websocket("/ws/realtime")
async def websocket_endpoint(websocket: WebSocket):
    session_id = f"session_{int(time.time() * 1000)}"
    
    try:
        await manager.connect(websocket, session_id)
        
        # 等待初始化消息
        init_message = await websocket.receive_text()
        init_data = json.loads(init_message)
        
        if init_data.get('type') == 'init':
            # 发送确认
            await manager.send_message(session_id, {
                'type': 'init_ack',
                'timestamp': int(time.time() * 1000),
                'payload': {
                    'status': 'success',
                    'sessionId': session_id,
                    'config': {
                        'maxAudioChunkSize': 8192,
                        'heartbeatInterval': 30000
                    }
                }
            })
        
        # 获取豆包连接
        doubao_client = await connection_pool.acquire()
        
        # 处理消息循环
        while True:
            message = await websocket.receive()
            
            if 'text' in message:
                # 控制消息
                control_data = json.loads(message['text'])
                await handle_control_message(
                    session_id, control_data, doubao_client
                )
            
            elif 'bytes' in message:
                # 音频数据
                audio_data = message['bytes']
                await handle_audio_data(
                    session_id, audio_data, doubao_client
                )
    
    except WebSocketDisconnect:
        print(f"客户端断开: {session_id}")
    
    except Exception as e:
        print(f"错误: {e}")
        await manager.send_message(session_id, {
            'type': 'error',
            'payload': {
                'code': 'INTERNAL_ERROR',
                'message': str(e)
            }
        })
    
    finally:
        manager.disconnect(session_id)
        if doubao_client:
            await connection_pool.release(doubao_client)

async def handle_control_message(session_id: str, message: dict, doubao_client):
    msg_type = message.get('type')
    
    if msg_type == 'start_recording':
        await manager.send_message(session_id, {
            'type': 'recording_started',
            'payload': {'status': 'ready'}
        })
        
        await manager.send_message(session_id, {
            'type': 'state_change',
            'payload': {'state': 'listening'}
        })
    
    elif msg_type == 'stop_recording':
        await manager.send_message(session_id, {
            'type': 'recording_stopped',
            'payload': {'status': 'success'}
        })

async def handle_audio_data(session_id: str, audio_data: bytes, doubao_client):
    # 转发给豆包API
    await doubao_client.send_audio(audio_data)
    
    # 接收豆包响应并转发给客户端
    response_audio = await doubao_client.receive_audio()
    await manager.send_audio(session_id, response_audio)

13. 测试工具

13.1 WebSocket测试工具

推荐工具:

  • Postman: 支持WebSocket测试
  • wscat: 命令行工具
  • websocat: 功能强大的命令行工具

wscat示例:

# 安装
npm install -g wscat

# 连接测试
wscat -c wss://api.yourdomain.com/ws/realtime

# 发送消息
> {"type":"init","payload":{"version":"1.0"}}

13.2 音频测试

# 发送测试音频文件
websocat wss://api.yourdomain.com/ws/realtime < test_audio.pcm

14. FAQ

Q1: 为什么选择WebSocket而不是HTTP轮询?

A: WebSocket提供全双工通信,延迟更低,适合实时音频传输。HTTP轮询会引入额外的延迟和服务器负载。

Q2: 音频数据需要加消息头吗?

A: 推荐直接发送原始PCM/Opus数据,不需要额外封装。服务端通过消息类型(文本/二进制)区分控制消息和音频数据。

Q3: 如何处理网络抖动?

A:

  • 客户端使用音频缓冲队列
  • 服务端使用连接池复用
  • 实现自动重连机制
  • 监控并上报延迟指标

Q4: 移动端网络切换如何处理?

A:

  • 监听网络变化事件
  • 自动触发重连
  • 保存会话状态
  • 提示用户网络状态

15. 版本历史

版本 日期 变更说明
v1.0 2025-10-08 初始版本

文档维护者: 开发团队
最后更新: 2025-10-08
反馈邮箱: dev@yourdomain.com